Path: blob/main/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts
4780 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 { $, append, EventType, addDisposableListener, EventHelper, disposableWindowInterval, getWindow } from '../../../../../base/browser/dom.js';6import { Gesture, EventType as TouchEventType } from '../../../../../base/browser/touch.js';7import { ActionBar } from '../../../../../base/browser/ui/actionbar/actionbar.js';8import { Button } from '../../../../../base/browser/ui/button/button.js';9import { renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js';10import { Checkbox } from '../../../../../base/browser/ui/toggle/toggle.js';11import { IAction, toAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../../base/common/actions.js';12import { cancelOnDispose } from '../../../../../base/common/cancellation.js';13import { Codicon } from '../../../../../base/common/codicons.js';14import { safeIntl } from '../../../../../base/common/date.js';15import { MarkdownString } from '../../../../../base/common/htmlContent.js';16import { MutableDisposable, DisposableStore } from '../../../../../base/common/lifecycle.js';17import { parseLinkedText } from '../../../../../base/common/linkedText.js';18import { language } from '../../../../../base/common/platform.js';19import { ThemeIcon } from '../../../../../base/common/themables.js';20import { isObject } from '../../../../../base/common/types.js';21import { URI } from '../../../../../base/common/uri.js';22import { IInlineCompletionsService } from '../../../../../editor/browser/services/inlineCompletionsService.js';23import { ILanguageService } from '../../../../../editor/common/languages/language.js';24import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js';25import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';26import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';27import * as languages from '../../../../../editor/common/languages.js';28import { localize } from '../../../../../nls.js';29import { ICommandService } from '../../../../../platform/commands/common/commands.js';30import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';31import { IHoverService, nativeHoverDelegate } from '../../../../../platform/hover/browser/hover.js';32import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.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 { defaultButtonStyles, defaultCheckboxStyles } from '../../../../../platform/theme/browser/defaultStyles.js';37import { DomWidget } from '../../../../../platform/domWidget/browser/domWidget.js';38import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js';39import { IChatEntitlementService, ChatEntitlementService, ChatEntitlement, IQuotaSnapshot } from '../../../../services/chat/common/chatEntitlementService.js';40import { IEditorService } from '../../../../services/editor/common/editorService.js';41import { IChatSessionsService } from '../../common/chatSessionsService.js';42import { isNewUser, isCompletionsEnabled } from './chatStatus.js';43import { IChatStatusItemService, ChatStatusEntry } from './chatStatusItemService.js';44import product from '../../../../../platform/product/common/product.js';45import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js';46import { Color } from '../../../../../base/common/color.js';47import { IViewsService } from '../../../../services/views/common/viewsService.js';48import { ChatViewId } from '../chat.js';4950const defaultChat = product.defaultChatAgent;5152interface ISettingsAccessor {53readSetting: () => boolean;54writeSetting: (value: boolean) => Promise<void>;55}56type ChatSettingChangedClassification = {57owner: 'bpasero';58comment: 'Provides insight into chat settings changed from the chat status entry.';59settingIdentifier: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the setting that changed.' };60settingMode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The optional editor language for which the setting changed.' };61settingEnablement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setting got enabled or disabled.' };62};63type ChatSettingChangedEvent = {64settingIdentifier: string;65settingMode?: string;66settingEnablement: 'enabled' | 'disabled';67};6869const gaugeForeground = registerColor('gauge.foreground', {70dark: inputValidationInfoBorder,71light: inputValidationInfoBorder,72hcDark: contrastBorder,73hcLight: contrastBorder74}, localize('gaugeForeground', "Gauge foreground color."));7576registerColor('gauge.background', {77dark: transparent(gaugeForeground, 0.3),78light: transparent(gaugeForeground, 0.3),79hcDark: Color.white,80hcLight: Color.white81}, localize('gaugeBackground', "Gauge background color."));8283registerColor('gauge.border', {84dark: null,85light: null,86hcDark: contrastBorder,87hcLight: contrastBorder88}, localize('gaugeBorder', "Gauge border color."));8990const gaugeWarningForeground = registerColor('gauge.warningForeground', {91dark: inputValidationWarningBorder,92light: inputValidationWarningBorder,93hcDark: contrastBorder,94hcLight: contrastBorder95}, localize('gaugeWarningForeground', "Gauge warning foreground color."));9697registerColor('gauge.warningBackground', {98dark: transparent(gaugeWarningForeground, 0.3),99light: transparent(gaugeWarningForeground, 0.3),100hcDark: Color.white,101hcLight: Color.white102}, localize('gaugeWarningBackground', "Gauge warning background color."));103104const gaugeErrorForeground = registerColor('gauge.errorForeground', {105dark: inputValidationErrorBorder,106light: inputValidationErrorBorder,107hcDark: contrastBorder,108hcLight: contrastBorder109}, localize('gaugeErrorForeground', "Gauge error foreground color."));110111registerColor('gauge.errorBackground', {112dark: transparent(gaugeErrorForeground, 0.3),113light: transparent(gaugeErrorForeground, 0.3),114hcDark: Color.white,115hcLight: Color.white116}, localize('gaugeErrorBackground', "Gauge error background color."));117118export class ChatStatusDashboard extends DomWidget {119120readonly element = $('div.chat-status-bar-entry-tooltip');121122private readonly dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' });123private readonly dateTimeFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });124private readonly quotaPercentageFormatter = safeIntl.NumberFormat(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 0 });125private readonly quotaOverageFormatter = safeIntl.NumberFormat(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 0 });126127constructor(128@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,129@IChatStatusItemService private readonly chatStatusItemService: IChatStatusItemService,130@ICommandService private readonly commandService: ICommandService,131@IConfigurationService private readonly configurationService: IConfigurationService,132@IEditorService private readonly editorService: IEditorService,133@IHoverService private readonly hoverService: IHoverService,134@ILanguageService private readonly languageService: ILanguageService,135@IOpenerService private readonly openerService: IOpenerService,136@ITelemetryService private readonly telemetryService: ITelemetryService,137@ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService,138@IInlineCompletionsService private readonly inlineCompletionsService: IInlineCompletionsService,139@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,140@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,141@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,142@IQuickInputService private readonly quickInputService: IQuickInputService,143@IViewsService private readonly viewService: IViewsService,144) {145super();146147this.render();148}149150private render(): void {151const token = cancelOnDispose(this._store);152153let needsSeparator = false;154const addSeparator = (label?: string, action?: IAction) => {155if (needsSeparator) {156this.element.appendChild($('hr'));157}158159if (label || action) {160this.renderHeader(this.element, this._store, label ?? '', action);161}162163needsSeparator = true;164};165166// Quota Indicator167const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota, resetDate, resetDateHasTime } = this.chatEntitlementService.quotas;168if (chatQuota || completionsQuota || premiumChatQuota) {169addSeparator(localize('usageTitle', "Copilot Usage"), toAction({170id: 'workbench.action.manageCopilot',171label: localize('quotaLabel', "Manage Chat"),172tooltip: localize('quotaTooltip', "Manage Chat"),173class: ThemeIcon.asClassName(Codicon.settings),174run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageSettingsUrl))),175}));176177const completionsQuotaIndicator = completionsQuota && (completionsQuota.total > 0 || completionsQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, completionsQuota, localize('completionsLabel', "Inline Suggestions"), false) : undefined;178const chatQuotaIndicator = chatQuota && (chatQuota.total > 0 || chatQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, chatQuota, localize('chatsLabel', "Chat messages"), false) : undefined;179const premiumChatQuotaIndicator = premiumChatQuota && (premiumChatQuota.total > 0 || premiumChatQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, premiumChatQuota, localize('premiumChatsLabel', "Premium requests"), true) : undefined;180181if (resetDate) {182this.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)))));183}184185if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && (Number(chatQuota?.percentRemaining) <= 25 || Number(completionsQuota?.percentRemaining) <= 25)) {186const upgradeProButton = this._store.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate, secondary: this.canUseChat() /* use secondary color when chat can still be used */ }));187upgradeProButton.label = localize('upgradeToCopilotPro', "Upgrade to GitHub Copilot Pro");188this._store.add(upgradeProButton.onDidClick(() => this.runCommandAndClose('workbench.action.chat.upgradePlan')));189}190191(async () => {192await this.chatEntitlementService.update(token);193if (token.isCancellationRequested) {194return;195}196197const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota } = this.chatEntitlementService.quotas;198if (completionsQuota) {199completionsQuotaIndicator?.(completionsQuota);200}201if (chatQuota) {202chatQuotaIndicator?.(chatQuota);203}204if (premiumChatQuota) {205premiumChatQuotaIndicator?.(premiumChatQuota);206}207})();208}209210// Anonymous Indicator211else if (this.chatEntitlementService.anonymous && this.chatEntitlementService.sentiment.installed) {212addSeparator(localize('anonymousTitle', "Copilot Usage"));213214this.createQuotaIndicator(this.element, this._store, localize('quotaLimited', "Limited"), localize('completionsLabel', "Inline Suggestions"), false);215this.createQuotaIndicator(this.element, this._store, localize('quotaLimited', "Limited"), localize('chatsLabel', "Chat messages"), false);216}217218// Chat sessions219{220const inProgress = this.chatSessionsService.getInProgress();221if (inProgress.some(item => item.count > 0)) {222223addSeparator(localize('chatAgentSessionsTitle', "Agent Sessions"), toAction({224id: 'workbench.view.chat.status.sessions',225label: localize('viewChatSessionsLabel', "View Agent Sessions"),226tooltip: localize('viewChatSessionsTooltip', "View Agent Sessions"),227class: ThemeIcon.asClassName(Codicon.eye),228run: () => {229this.viewService.openView(ChatViewId, true);230this.hoverService.hideHover(true);231}232}));233234for (const { displayName, count } of inProgress) {235if (count > 0) {236const text = localize('inProgressChatSession', "$(loading~spin) {0} in progress", displayName);237const chatSessionsElement = this.element.appendChild($('div.description'));238const parts = renderLabelWithIcons(text);239chatSessionsElement.append(...parts);240}241}242}243}244245// Contributions246{247for (const item of this.chatStatusItemService.getEntries()) {248addSeparator();249250const itemDisposables = this._store.add(new MutableDisposable());251252let rendered = this.renderContributedChatStatusItem(item);253itemDisposables.value = rendered.disposables;254this.element.appendChild(rendered.element);255256this._store.add(this.chatStatusItemService.onDidChange(e => {257if (e.entry.id === item.id) {258const previousElement = rendered.element;259260rendered = this.renderContributedChatStatusItem(e.entry);261itemDisposables.value = rendered.disposables;262263previousElement.replaceWith(rendered.element);264}265}));266}267}268269// Settings270{271const chatSentiment = this.chatEntitlementService.sentiment;272addSeparator(localize('inlineSuggestions', "Inline Suggestions"), chatSentiment.installed && !chatSentiment.disabled && !chatSentiment.untrusted ? toAction({273id: 'workbench.action.openChatSettings',274label: localize('settingsLabel', "Settings"),275tooltip: localize('settingsTooltip', "Open Settings"),276class: ThemeIcon.asClassName(Codicon.settingsGear),277run: () => this.runCommandAndClose(() => this.commandService.executeCommand('workbench.action.openSettings', { query: `@id:${defaultChat.completionsEnablementSetting} @id:${defaultChat.nextEditSuggestionsSetting}` })),278}) : undefined);279280this.createSettings(this.element, this._store);281}282283// Model Selection284{285const providers = this.languageFeaturesService.inlineCompletionsProvider.allNoModel();286const provider = providers.find(p => p.modelInfo && p.modelInfo.models.length > 0);287288if (provider) {289const modelInfo = provider.modelInfo!;290const currentModel = modelInfo.models.find(m => m.id === modelInfo.currentModelId);291292if (currentModel) {293const modelContainer = this.element.appendChild($('div.model-selection'));294295modelContainer.appendChild($('span.model-text', undefined, localize('modelLabel', "Model: {0}", currentModel.name)));296297const actionBar = modelContainer.appendChild($('div.model-action-bar'));298const toolbar = this._store.add(new ActionBar(actionBar, { hoverDelegate: nativeHoverDelegate }));299toolbar.push([toAction({300id: 'workbench.action.selectInlineCompletionsModel',301label: localize('selectModel', "Select Model"),302tooltip: localize('selectModel', "Select Model"),303class: ThemeIcon.asClassName(Codicon.gear),304run: async () => {305await this.showModelPicker(provider);306}307})], { icon: true, label: false });308}309}310}311312// Completions Snooze313if (this.canUseChat()) {314const snooze = append(this.element, $('div.snooze-completions'));315this.createCompletionsSnooze(snooze, localize('settings.snooze', "Snooze"), this._store);316}317318// New to Chat / Signed out319{320const newUser = isNewUser(this.chatEntitlementService);321const anonymousUser = this.chatEntitlementService.anonymous;322const disabled = this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted;323const signedOut = this.chatEntitlementService.entitlement === ChatEntitlement.Unknown;324if (newUser || signedOut || disabled) {325addSeparator();326327let descriptionText: string | MarkdownString;328let descriptionClass = '.description';329if (newUser && anonymousUser) {330descriptionText = 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 });331descriptionClass = `${descriptionClass}.terms`;332} else if (newUser) {333descriptionText = localize('activateDescription', "Set up Copilot to use AI features.");334} else if (anonymousUser) {335descriptionText = localize('enableMoreDescription', "Sign in to enable more Copilot AI features.");336} else if (disabled) {337descriptionText = localize('enableDescription', "Enable Copilot to use AI features.");338} else {339descriptionText = localize('signInDescription', "Sign in to use Copilot AI features.");340}341342let buttonLabel: string;343if (newUser) {344buttonLabel = localize('enableAIFeatures', "Use AI Features");345} else if (anonymousUser) {346buttonLabel = localize('enableMoreAIFeatures', "Enable more AI Features");347} else if (disabled) {348buttonLabel = localize('enableCopilotButton', "Enable AI Features");349} else {350buttonLabel = localize('signInToUseAIFeatures', "Sign in to use AI Features");351}352353let commandId: string;354if (newUser && anonymousUser) {355commandId = 'workbench.action.chat.triggerSetupAnonymousWithoutDialog';356} else {357commandId = 'workbench.action.chat.triggerSetup';358}359360if (typeof descriptionText === 'string') {361this.element.appendChild($(`div${descriptionClass}`, undefined, descriptionText));362} else {363this.element.appendChild($(`div${descriptionClass}`, undefined, this._store.add(this.markdownRendererService.render(descriptionText)).element));364}365366const button = this._store.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate }));367button.label = buttonLabel;368this._store.add(button.onDidClick(() => this.runCommandAndClose(commandId)));369}370}371}372373private canUseChat(): boolean {374if (!this.chatEntitlementService.sentiment.installed || this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted) {375return false; // chat not installed or not enabled376}377378if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown || this.chatEntitlementService.entitlement === ChatEntitlement.Available) {379return this.chatEntitlementService.anonymous; // signed out or not-yet-signed-up users can only use Chat if anonymous access is allowed380}381382if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && this.chatEntitlementService.quotas.chat?.percentRemaining === 0 && this.chatEntitlementService.quotas.completions?.percentRemaining === 0) {383return false; // free user with no quota left384}385386return true;387}388389private renderHeader(container: HTMLElement, disposables: DisposableStore, label: string, action?: IAction): void {390const header = container.appendChild($('div.header', undefined, label ?? ''));391392if (action) {393const toolbar = disposables.add(new ActionBar(header, { hoverDelegate: nativeHoverDelegate }));394toolbar.push([action], { icon: true, label: false });395}396}397398private renderContributedChatStatusItem(item: ChatStatusEntry): { element: HTMLElement; disposables: DisposableStore } {399const disposables = new DisposableStore();400401const itemElement = $('div.contribution');402403const headerLabel = typeof item.label === 'string' ? item.label : item.label.label;404const headerLink = typeof item.label === 'string' ? undefined : item.label.link;405this.renderHeader(itemElement, disposables, headerLabel, headerLink ? toAction({406id: 'workbench.action.openChatStatusItemLink',407label: localize('learnMore', "Learn More"),408tooltip: localize('learnMore', "Learn More"),409class: ThemeIcon.asClassName(Codicon.linkExternal),410run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(headerLink))),411}) : undefined);412413const itemBody = itemElement.appendChild($('div.body'));414415const description = itemBody.appendChild($('span.description'));416this.renderTextPlus(description, item.description, disposables);417418if (item.detail) {419const detail = itemBody.appendChild($('div.detail-item'));420this.renderTextPlus(detail, item.detail, disposables);421}422423return { element: itemElement, disposables };424}425426private renderTextPlus(target: HTMLElement, text: string, store: DisposableStore): void {427for (const node of parseLinkedText(text).nodes) {428if (typeof node === 'string') {429const parts = renderLabelWithIcons(node);430target.append(...parts);431} else {432store.add(new Link(target, node, undefined, this.hoverService, this.openerService));433}434}435}436437private runCommandAndClose(commandOrFn: string | Function, ...args: unknown[]): void {438if (typeof commandOrFn === 'function') {439commandOrFn(...args);440} else {441this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: commandOrFn, from: 'chat-status' });442this.commandService.executeCommand(commandOrFn, ...args);443}444445this.hoverService.hideHover(true);446}447448private createQuotaIndicator(container: HTMLElement, disposables: DisposableStore, quota: IQuotaSnapshot | string, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot | string) => void {449const quotaValue = $('span.quota-value');450const quotaBit = $('div.quota-bit');451const overageLabel = $('span.overage-label');452453const quotaIndicator = container.appendChild($('div.quota-indicator', undefined,454$('div.quota-label', undefined,455$('span', undefined, label),456quotaValue457),458$('div.quota-bar', undefined,459quotaBit460),461$('div.description', undefined,462overageLabel463)464));465466if (supportsOverage && (this.chatEntitlementService.entitlement === ChatEntitlement.Pro || this.chatEntitlementService.entitlement === ChatEntitlement.ProPlus)) {467const manageOverageButton = disposables.add(new Button(quotaIndicator, { ...defaultButtonStyles, secondary: true, hoverDelegate: nativeHoverDelegate }));468manageOverageButton.label = localize('enableAdditionalUsage', "Manage paid premium requests");469disposables.add(manageOverageButton.onDidClick(() => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageOverageUrl)))));470}471472const update = (quota: IQuotaSnapshot | string) => {473quotaIndicator.classList.remove('error');474quotaIndicator.classList.remove('warning');475476let usedPercentage: number;477if (typeof quota === 'string' || quota.unlimited) {478usedPercentage = 0;479} else {480usedPercentage = Math.max(0, 100 - quota.percentRemaining);481}482483if (typeof quota === 'string') {484quotaValue.textContent = quota;485} else if (quota.unlimited) {486quotaValue.textContent = localize('quotaUnlimited', "Included");487} else if (quota.overageCount) {488quotaValue.textContent = localize('quotaDisplayWithOverage', "+{0} requests", this.quotaOverageFormatter.value.format(quota.overageCount));489} else {490quotaValue.textContent = localize('quotaDisplay', "{0}%", this.quotaPercentageFormatter.value.format(usedPercentage));491}492493quotaBit.style.width = `${usedPercentage}%`;494495if (usedPercentage >= 90) {496quotaIndicator.classList.add('error');497} else if (usedPercentage >= 75) {498quotaIndicator.classList.add('warning');499}500501if (supportsOverage) {502if (typeof quota !== 'string' && quota?.overageEnabled) {503overageLabel.textContent = localize('additionalUsageEnabled', "Additional paid premium requests enabled.");504} else {505overageLabel.textContent = localize('additionalUsageDisabled', "Additional paid premium requests disabled.");506}507} else {508overageLabel.textContent = '';509}510};511512update(quota);513514return update;515}516517private createSettings(container: HTMLElement, disposables: DisposableStore): HTMLElement {518const modeId = this.editorService.activeTextEditorLanguageId;519const settings = container.appendChild($('div.settings'));520521// --- Inline Suggestions522{523const globalSetting = append(settings, $('div.setting'));524this.createInlineSuggestionsSetting(globalSetting, localize('settings.codeCompletions.allFiles', "All files"), '*', disposables);525526if (modeId) {527const languageSetting = append(settings, $('div.setting'));528this.createInlineSuggestionsSetting(languageSetting, localize('settings.codeCompletions.language', "{0}", this.languageService.getLanguageName(modeId) ?? modeId), modeId, disposables);529}530}531532// --- Next edit suggestions533{534const setting = append(settings, $('div.setting'));535this.createNextEditSuggestionsSetting(setting, localize('settings.nextEditSuggestions', "Next edit suggestions"), this.getCompletionsSettingAccessor(modeId), disposables);536}537538return settings;539}540541private createSetting(container: HTMLElement, settingIdsToReEvaluate: string[], label: string, accessor: ISettingsAccessor, disposables: DisposableStore): Checkbox {542const checkbox = disposables.add(new Checkbox(label, Boolean(accessor.readSetting()), { ...defaultCheckboxStyles }));543container.appendChild(checkbox.domNode);544545const settingLabel = append(container, $('span.setting-label', undefined, label));546disposables.add(Gesture.addTarget(settingLabel));547[EventType.CLICK, TouchEventType.Tap].forEach(eventType => {548disposables.add(addDisposableListener(settingLabel, eventType, e => {549if (checkbox?.enabled) {550EventHelper.stop(e, true);551552checkbox.checked = !checkbox.checked;553accessor.writeSetting(checkbox.checked);554checkbox.focus();555}556}));557});558559disposables.add(checkbox.onChange(() => {560accessor.writeSetting(checkbox.checked);561}));562563disposables.add(this.configurationService.onDidChangeConfiguration(e => {564if (settingIdsToReEvaluate.some(id => e.affectsConfiguration(id))) {565checkbox.checked = Boolean(accessor.readSetting());566}567}));568569if (!this.canUseChat()) {570container.classList.add('disabled');571checkbox.disable();572checkbox.checked = false;573}574575return checkbox;576}577578private createInlineSuggestionsSetting(container: HTMLElement, label: string, modeId: string | undefined, disposables: DisposableStore): void {579this.createSetting(container, [defaultChat.completionsEnablementSetting], label, this.getCompletionsSettingAccessor(modeId), disposables);580}581582private getCompletionsSettingAccessor(modeId = '*'): ISettingsAccessor {583const settingId = defaultChat.completionsEnablementSetting;584585return {586readSetting: () => isCompletionsEnabled(this.configurationService, modeId),587writeSetting: (value: boolean) => {588this.telemetryService.publicLog2<ChatSettingChangedEvent, ChatSettingChangedClassification>('chatStatus.settingChanged', {589settingIdentifier: settingId,590settingMode: modeId,591settingEnablement: value ? 'enabled' : 'disabled'592});593594let result = this.configurationService.getValue<Record<string, boolean>>(settingId);595if (!isObject(result)) {596result = Object.create(null);597}598599return this.configurationService.updateValue(settingId, { ...result, [modeId]: value });600}601};602}603604private createNextEditSuggestionsSetting(container: HTMLElement, label: string, completionsSettingAccessor: ISettingsAccessor, disposables: DisposableStore): void {605const nesSettingId = defaultChat.nextEditSuggestionsSetting;606const completionsSettingId = defaultChat.completionsEnablementSetting;607const resource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });608609const checkbox = this.createSetting(container, [nesSettingId, completionsSettingId], label, {610readSetting: () => completionsSettingAccessor.readSetting() && this.textResourceConfigurationService.getValue<boolean>(resource, nesSettingId),611writeSetting: (value: boolean) => {612this.telemetryService.publicLog2<ChatSettingChangedEvent, ChatSettingChangedClassification>('chatStatus.settingChanged', {613settingIdentifier: nesSettingId,614settingEnablement: value ? 'enabled' : 'disabled'615});616617return this.textResourceConfigurationService.updateValue(resource, nesSettingId, value);618}619}, disposables);620621// enablement of NES depends on completions setting622// so we have to update our checkbox state accordingly623if (!completionsSettingAccessor.readSetting()) {624container.classList.add('disabled');625checkbox.disable();626}627628disposables.add(this.configurationService.onDidChangeConfiguration(e => {629if (e.affectsConfiguration(completionsSettingId)) {630if (completionsSettingAccessor.readSetting() && this.canUseChat()) {631checkbox.enable();632container.classList.remove('disabled');633} else {634checkbox.disable();635container.classList.add('disabled');636}637}638}));639}640641private createCompletionsSnooze(container: HTMLElement, label: string, disposables: DisposableStore): void {642const isEnabled = () => {643const completionsEnabled = isCompletionsEnabled(this.configurationService);644const completionsEnabledActiveLanguage = isCompletionsEnabled(this.configurationService, this.editorService.activeTextEditorLanguageId);645return completionsEnabled || completionsEnabledActiveLanguage;646};647648const button = disposables.add(new Button(container, { disabled: !isEnabled(), ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate, secondary: true }));649650const timerDisplay = container.appendChild($('span.snooze-label'));651652const actionBar = container.appendChild($('div.snooze-action-bar'));653const toolbar = disposables.add(new ActionBar(actionBar, { hoverDelegate: nativeHoverDelegate }));654const cancelAction = toAction({655id: 'workbench.action.cancelSnoozeStatusBarLink',656label: localize('cancelSnooze', "Cancel Snooze"),657run: () => this.inlineCompletionsService.cancelSnooze(),658class: ThemeIcon.asClassName(Codicon.stopCircle)659});660661const update = (isEnabled: boolean) => {662container.classList.toggle('disabled', !isEnabled);663toolbar.clear();664665const timeLeftMs = this.inlineCompletionsService.snoozeTimeLeft;666if (!isEnabled || timeLeftMs <= 0) {667timerDisplay.textContent = localize('completions.snooze5minutesTitle', "Hide suggestions for 5 min");668timerDisplay.title = '';669button.label = label;670button.setTitle(localize('completions.snooze5minutes', "Hide inline suggestions for 5 min"));671return true;672}673674const timeLeftSeconds = Math.ceil(timeLeftMs / 1000);675const minutes = Math.floor(timeLeftSeconds / 60);676const seconds = timeLeftSeconds % 60;677678timerDisplay.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds} ${localize('completions.remainingTime', "remaining")}`;679timerDisplay.title = localize('completions.snoozeTimeDescription', "Inline suggestions are hidden for the remaining duration");680button.label = localize('completions.plus5min', "+5 min");681button.setTitle(localize('completions.snoozeAdditional5minutes', "Snooze additional 5 min"));682toolbar.push([cancelAction], { icon: true, label: false });683684return false;685};686687// Update every second if there's time remaining688const timerDisposables = disposables.add(new DisposableStore());689function updateIntervalTimer() {690timerDisposables.clear();691const enabled = isEnabled();692693if (update(enabled)) {694return;695}696697timerDisposables.add(disposableWindowInterval(698getWindow(container),699() => update(enabled),7001000701));702}703updateIntervalTimer();704705disposables.add(button.onDidClick(() => {706this.inlineCompletionsService.snooze();707update(isEnabled());708}));709710disposables.add(this.configurationService.onDidChangeConfiguration(e => {711if (e.affectsConfiguration(defaultChat.completionsEnablementSetting)) {712button.enabled = isEnabled();713}714updateIntervalTimer();715}));716717disposables.add(this.inlineCompletionsService.onDidChangeIsSnoozing(e => {718updateIntervalTimer();719}));720}721722private async showModelPicker(provider: languages.InlineCompletionsProvider): Promise<void> {723if (!provider.modelInfo || !provider.setModelId) {724return;725}726727const modelInfo = provider.modelInfo;728const items: IQuickPickItem[] = modelInfo.models.map(model => ({729id: model.id,730label: model.name,731description: model.id === modelInfo.currentModelId ? localize('currentModel.description', "Currently selected") : undefined,732picked: model.id === modelInfo.currentModelId733}));734735const selected = await this.quickInputService.pick(items, {736placeHolder: localize('selectModelFor', "Select a model for {0}", provider.displayName || 'inline completions'),737canPickMany: false738});739740if (selected && selected.id && selected.id !== modelInfo.currentModelId) {741await provider.setModelId(selected.id);742}743744this.hoverService.hideHover(true);745}746}747748749