Path: blob/main/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts
5272 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, getChatPlanName } from '../../../../services/chat/common/chatEntitlementService.js';40import { IEditorService } from '../../../../services/editor/common/editorService.js';41import { IChatSessionsService } from '../../common/chatSessionsService.js';42import { isNewUser } 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';49import { isCompletionsEnabled } from '../../../../../editor/common/services/completionsEnablement.js';5051const defaultChat = product.defaultChatAgent;5253interface ISettingsAccessor {54readSetting: () => boolean;55writeSetting: (value: boolean) => Promise<void>;56}57type ChatSettingChangedClassification = {58owner: 'bpasero';59comment: 'Provides insight into chat settings changed from the chat status entry.';60settingIdentifier: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the setting that changed.' };61settingMode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The optional editor language for which the setting changed.' };62settingEnablement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setting got enabled or disabled.' };63};64type ChatSettingChangedEvent = {65settingIdentifier: string;66settingMode?: string;67settingEnablement: 'enabled' | 'disabled';68};6970const gaugeForeground = registerColor('gauge.foreground', {71dark: inputValidationInfoBorder,72light: inputValidationInfoBorder,73hcDark: contrastBorder,74hcLight: contrastBorder75}, localize('gaugeForeground', "Gauge foreground color."));7677registerColor('gauge.background', {78dark: transparent(gaugeForeground, 0.3),79light: transparent(gaugeForeground, 0.3),80hcDark: Color.white,81hcLight: Color.white82}, localize('gaugeBackground', "Gauge background color."));8384registerColor('gauge.border', {85dark: null,86light: null,87hcDark: contrastBorder,88hcLight: contrastBorder89}, localize('gaugeBorder', "Gauge border color."));9091const gaugeWarningForeground = registerColor('gauge.warningForeground', {92dark: inputValidationWarningBorder,93light: inputValidationWarningBorder,94hcDark: contrastBorder,95hcLight: contrastBorder96}, localize('gaugeWarningForeground', "Gauge warning foreground color."));9798registerColor('gauge.warningBackground', {99dark: transparent(gaugeWarningForeground, 0.3),100light: transparent(gaugeWarningForeground, 0.3),101hcDark: Color.white,102hcLight: Color.white103}, localize('gaugeWarningBackground', "Gauge warning background color."));104105const gaugeErrorForeground = registerColor('gauge.errorForeground', {106dark: inputValidationErrorBorder,107light: inputValidationErrorBorder,108hcDark: contrastBorder,109hcLight: contrastBorder110}, localize('gaugeErrorForeground', "Gauge error foreground color."));111112registerColor('gauge.errorBackground', {113dark: transparent(gaugeErrorForeground, 0.3),114light: transparent(gaugeErrorForeground, 0.3),115hcDark: Color.white,116hcLight: Color.white117}, localize('gaugeErrorBackground', "Gauge error background color."));118119export class ChatStatusDashboard extends DomWidget {120121readonly element = $('div.chat-status-bar-entry-tooltip');122123private readonly dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' });124private readonly dateTimeFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });125private readonly quotaPercentageFormatter = safeIntl.NumberFormat(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 0 });126private readonly quotaOverageFormatter = safeIntl.NumberFormat(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 0 });127128constructor(129@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,130@IChatStatusItemService private readonly chatStatusItemService: IChatStatusItemService,131@ICommandService private readonly commandService: ICommandService,132@IConfigurationService private readonly configurationService: IConfigurationService,133@IEditorService private readonly editorService: IEditorService,134@IHoverService private readonly hoverService: IHoverService,135@ILanguageService private readonly languageService: ILanguageService,136@IOpenerService private readonly openerService: IOpenerService,137@ITelemetryService private readonly telemetryService: ITelemetryService,138@ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService,139@IInlineCompletionsService private readonly inlineCompletionsService: IInlineCompletionsService,140@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,141@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,142@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,143@IQuickInputService private readonly quickInputService: IQuickInputService,144@IViewsService private readonly viewService: IViewsService,145) {146super();147148this.render();149}150151private render(): void {152const token = cancelOnDispose(this._store);153154let needsSeparator = false;155const addSeparator = (label?: string, action?: IAction) => {156if (needsSeparator) {157this.element.appendChild($('hr'));158}159160if (label || action) {161this.renderHeader(this.element, this._store, label ?? '', action);162}163164needsSeparator = true;165};166167// Quota Indicator168const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota, resetDate, resetDateHasTime } = this.chatEntitlementService.quotas;169if (chatQuota || completionsQuota || premiumChatQuota) {170const usageTitle = this.getUsageTitle();171addSeparator(usageTitle, toAction({172id: 'workbench.action.manageCopilot',173label: localize('quotaLabel', "Manage Chat"),174tooltip: localize('quotaTooltip', "Manage Chat"),175class: ThemeIcon.asClassName(Codicon.settings),176run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageSettingsUrl))),177}));178179const completionsQuotaIndicator = completionsQuota && (completionsQuota.total > 0 || completionsQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, completionsQuota, localize('completionsLabel', "Inline Suggestions"), false) : undefined;180const chatQuotaIndicator = chatQuota && (chatQuota.total > 0 || chatQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, chatQuota, localize('chatsLabel', "Chat messages"), false) : undefined;181const premiumChatQuotaIndicator = premiumChatQuota && (premiumChatQuota.total > 0 || premiumChatQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, premiumChatQuota, localize('premiumChatsLabel', "Premium requests"), true) : undefined;182183if (resetDate) {184this.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)))));185}186187if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && (Number(chatQuota?.percentRemaining) <= 25 || Number(completionsQuota?.percentRemaining) <= 25)) {188const upgradeProButton = this._store.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate, secondary: this.canUseChat() /* use secondary color when chat can still be used */ }));189upgradeProButton.label = localize('upgradeToCopilotPro', "Upgrade to GitHub Copilot Pro");190this._store.add(upgradeProButton.onDidClick(() => this.runCommandAndClose('workbench.action.chat.upgradePlan')));191}192193(async () => {194await this.chatEntitlementService.update(token);195if (token.isCancellationRequested) {196return;197}198199const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota } = this.chatEntitlementService.quotas;200if (completionsQuota) {201completionsQuotaIndicator?.(completionsQuota);202}203if (chatQuota) {204chatQuotaIndicator?.(chatQuota);205}206if (premiumChatQuota) {207premiumChatQuotaIndicator?.(premiumChatQuota);208}209})();210}211212// Anonymous Indicator213else if (this.chatEntitlementService.anonymous && this.chatEntitlementService.sentiment.installed) {214addSeparator(localize('anonymousTitle', "Copilot Usage"));215216this.createQuotaIndicator(this.element, this._store, localize('quotaLimited', "Limited"), localize('completionsLabel', "Inline Suggestions"), false);217this.createQuotaIndicator(this.element, this._store, localize('quotaLimited', "Limited"), localize('chatsLabel', "Chat messages"), false);218}219220// Chat sessions221{222const inProgress = this.chatSessionsService.getInProgress();223if (inProgress.some(item => item.count > 0)) {224225addSeparator(localize('chatAgentSessionsTitle', "Agent Sessions"), toAction({226id: 'workbench.view.chat.status.sessions',227label: localize('viewChatSessionsLabel', "View Agent Sessions"),228tooltip: localize('viewChatSessionsTooltip', "View Agent Sessions"),229class: ThemeIcon.asClassName(Codicon.eye),230run: () => {231this.viewService.openView(ChatViewId, true);232this.hoverService.hideHover(true);233}234}));235236for (const { displayName, count } of inProgress) {237if (count > 0) {238const text = localize('inProgressChatSession', "$(loading~spin) {0} in progress", displayName);239const chatSessionsElement = this.element.appendChild($('div.description'));240const parts = renderLabelWithIcons(text);241chatSessionsElement.append(...parts);242}243}244}245}246247// Contributions248{249for (const item of this.chatStatusItemService.getEntries()) {250addSeparator();251252const itemDisposables = this._store.add(new MutableDisposable());253254let rendered = this.renderContributedChatStatusItem(item);255itemDisposables.value = rendered.disposables;256this.element.appendChild(rendered.element);257258this._store.add(this.chatStatusItemService.onDidChange(e => {259if (e.entry.id === item.id) {260const previousElement = rendered.element;261262rendered = this.renderContributedChatStatusItem(e.entry);263itemDisposables.value = rendered.disposables;264265previousElement.replaceWith(rendered.element);266}267}));268}269}270271// Settings272{273const chatSentiment = this.chatEntitlementService.sentiment;274addSeparator(localize('inlineSuggestions', "Inline Suggestions"), chatSentiment.installed && !chatSentiment.disabled && !chatSentiment.untrusted ? toAction({275id: 'workbench.action.openChatSettings',276label: localize('settingsLabel', "Settings"),277tooltip: localize('settingsTooltip', "Open Settings"),278class: ThemeIcon.asClassName(Codicon.settingsGear),279run: () => this.runCommandAndClose(() => this.commandService.executeCommand('workbench.action.openSettings', { query: `@id:${defaultChat.completionsEnablementSetting} @id:${defaultChat.nextEditSuggestionsSetting}` })),280}) : undefined);281282this.createSettings(this.element, this._store);283}284285// Model Selection286{287const providers = this.languageFeaturesService.inlineCompletionsProvider.allNoModel();288const provider = providers.find(p => p.modelInfo && p.modelInfo.models.length > 0);289290if (provider) {291const modelInfo = provider.modelInfo!;292const currentModel = modelInfo.models.find(m => m.id === modelInfo.currentModelId);293294if (currentModel) {295const modelContainer = this.element.appendChild($('div.model-selection'));296297modelContainer.appendChild($('span.model-text', undefined, localize('modelLabel', "Model: {0}", currentModel.name)));298299const actionBar = modelContainer.appendChild($('div.model-action-bar'));300const toolbar = this._store.add(new ActionBar(actionBar, { hoverDelegate: nativeHoverDelegate }));301toolbar.push([toAction({302id: 'workbench.action.selectInlineCompletionsModel',303label: localize('selectModel', "Select Model"),304tooltip: localize('selectModel', "Select Model"),305class: ThemeIcon.asClassName(Codicon.gear),306run: async () => {307await this.showModelPicker(provider);308}309})], { icon: true, label: false });310}311}312}313314// Completions Snooze315if (this.canUseChat()) {316const snooze = append(this.element, $('div.snooze-completions'));317this.createCompletionsSnooze(snooze, localize('settings.snooze', "Snooze"), this._store);318}319320// New to Chat / Signed out321{322const newUser = isNewUser(this.chatEntitlementService);323const anonymousUser = this.chatEntitlementService.anonymous;324const disabled = this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted;325const signedOut = this.chatEntitlementService.entitlement === ChatEntitlement.Unknown;326if (newUser || signedOut || disabled) {327addSeparator();328329let descriptionText: string | MarkdownString;330let descriptionClass = '.description';331if (newUser && anonymousUser) {332descriptionText = 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 });333descriptionClass = `${descriptionClass}.terms`;334} else if (newUser) {335descriptionText = localize('activateDescription', "Set up Copilot to use AI features.");336} else if (anonymousUser) {337descriptionText = localize('enableMoreDescription', "Sign in to enable more Copilot AI features.");338} else if (disabled) {339descriptionText = localize('enableDescription', "Enable Copilot to use AI features.");340} else {341descriptionText = localize('signInDescription', "Sign in to use Copilot AI features.");342}343344let buttonLabel: string;345if (newUser) {346buttonLabel = localize('enableAIFeatures', "Use AI Features");347} else if (anonymousUser) {348buttonLabel = localize('enableMoreAIFeatures', "Enable more AI Features");349} else if (disabled) {350buttonLabel = localize('enableCopilotButton', "Enable AI Features");351} else {352buttonLabel = localize('signInToUseAIFeatures', "Sign in to use AI Features");353}354355let commandId: string;356if (newUser && anonymousUser) {357commandId = 'workbench.action.chat.triggerSetupAnonymousWithoutDialog';358} else {359commandId = 'workbench.action.chat.triggerSetup';360}361362if (typeof descriptionText === 'string') {363this.element.appendChild($(`div${descriptionClass}`, undefined, descriptionText));364} else {365this.element.appendChild($(`div${descriptionClass}`, undefined, this._store.add(this.markdownRendererService.render(descriptionText)).element));366}367368const button = this._store.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate }));369button.label = buttonLabel;370this._store.add(button.onDidClick(() => this.runCommandAndClose(commandId)));371}372}373}374375private canUseChat(): boolean {376if (!this.chatEntitlementService.sentiment.installed || this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted) {377return false; // chat not installed or not enabled378}379380if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown || this.chatEntitlementService.entitlement === ChatEntitlement.Available) {381return this.chatEntitlementService.anonymous; // signed out or not-yet-signed-up users can only use Chat if anonymous access is allowed382}383384if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && this.chatEntitlementService.quotas.chat?.percentRemaining === 0 && this.chatEntitlementService.quotas.completions?.percentRemaining === 0) {385return false; // free user with no quota left386}387388return true;389}390391private getUsageTitle(): string {392const planName = getChatPlanName(this.chatEntitlementService.entitlement);393return localize('usageTitleWithPlan', "{0} Usage", planName);394}395396private renderHeader(container: HTMLElement, disposables: DisposableStore, label: string, action?: IAction): void {397const header = container.appendChild($('div.header', undefined, label ?? ''));398399if (action) {400const toolbar = disposables.add(new ActionBar(header, { hoverDelegate: nativeHoverDelegate }));401toolbar.push([action], { icon: true, label: false });402}403}404405private renderContributedChatStatusItem(item: ChatStatusEntry): { element: HTMLElement; disposables: DisposableStore } {406const disposables = new DisposableStore();407408const itemElement = $('div.contribution');409410const headerLabel = typeof item.label === 'string' ? item.label : item.label.label;411const headerLink = typeof item.label === 'string' ? undefined : item.label.link;412this.renderHeader(itemElement, disposables, headerLabel, headerLink ? toAction({413id: 'workbench.action.openChatStatusItemLink',414label: localize('learnMore', "Learn More"),415tooltip: localize('learnMore', "Learn More"),416class: ThemeIcon.asClassName(Codicon.linkExternal),417run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(headerLink))),418}) : undefined);419420const itemBody = itemElement.appendChild($('div.body'));421422const description = itemBody.appendChild($('span.description'));423this.renderTextPlus(description, item.description, disposables);424425if (item.detail) {426const detail = itemBody.appendChild($('div.detail-item'));427this.renderTextPlus(detail, item.detail, disposables);428}429430return { element: itemElement, disposables };431}432433private renderTextPlus(target: HTMLElement, text: string, store: DisposableStore): void {434for (const node of parseLinkedText(text).nodes) {435if (typeof node === 'string') {436const parts = renderLabelWithIcons(node);437target.append(...parts);438} else {439store.add(new Link(target, node, undefined, this.hoverService, this.openerService));440}441}442}443444private runCommandAndClose(commandOrFn: string | Function, ...args: unknown[]): void {445if (typeof commandOrFn === 'function') {446commandOrFn(...args);447} else {448this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: commandOrFn, from: 'chat-status' });449this.commandService.executeCommand(commandOrFn, ...args);450}451452this.hoverService.hideHover(true);453}454455private createQuotaIndicator(container: HTMLElement, disposables: DisposableStore, quota: IQuotaSnapshot | string, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot | string) => void {456const quotaValue = $('span.quota-value');457const quotaBit = $('div.quota-bit');458const overageLabel = $('span.overage-label');459460const quotaIndicator = container.appendChild($('div.quota-indicator', undefined,461$('div.quota-label', undefined,462$('span', undefined, label),463quotaValue464),465$('div.quota-bar', undefined,466quotaBit467),468$('div.description', undefined,469overageLabel470)471));472473if (supportsOverage && (this.chatEntitlementService.entitlement === ChatEntitlement.Pro || this.chatEntitlementService.entitlement === ChatEntitlement.ProPlus)) {474const manageOverageButton = disposables.add(new Button(quotaIndicator, { ...defaultButtonStyles, secondary: true, hoverDelegate: nativeHoverDelegate }));475manageOverageButton.label = localize('enableAdditionalUsage', "Manage paid premium requests");476disposables.add(manageOverageButton.onDidClick(() => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageOverageUrl)))));477}478479const update = (quota: IQuotaSnapshot | string) => {480quotaIndicator.classList.remove('error');481quotaIndicator.classList.remove('warning');482483let usedPercentage: number;484if (typeof quota === 'string' || quota.unlimited) {485usedPercentage = 0;486} else {487usedPercentage = Math.max(0, 100 - quota.percentRemaining);488}489490if (typeof quota === 'string') {491quotaValue.textContent = quota;492} else if (quota.unlimited) {493quotaValue.textContent = localize('quotaUnlimited', "Included");494} else if (quota.overageCount) {495quotaValue.textContent = localize('quotaDisplayWithOverage', "+{0} requests", this.quotaOverageFormatter.value.format(quota.overageCount));496} else {497quotaValue.textContent = localize('quotaDisplay', "{0}%", this.quotaPercentageFormatter.value.format(usedPercentage));498}499500quotaBit.style.width = `${usedPercentage}%`;501502if (usedPercentage >= 90) {503quotaIndicator.classList.add('error');504} else if (usedPercentage >= 75) {505quotaIndicator.classList.add('warning');506}507508if (supportsOverage) {509if (typeof quota !== 'string' && quota?.overageEnabled) {510overageLabel.textContent = localize('additionalUsageEnabled', "Additional paid premium requests enabled.");511} else {512overageLabel.textContent = localize('additionalUsageDisabled', "Additional paid premium requests disabled.");513}514} else {515overageLabel.textContent = '';516}517};518519update(quota);520521return update;522}523524private createSettings(container: HTMLElement, disposables: DisposableStore): HTMLElement {525const modeId = this.editorService.activeTextEditorLanguageId;526const settings = container.appendChild($('div.settings'));527528// --- Inline Suggestions529{530const globalSetting = append(settings, $('div.setting'));531this.createInlineSuggestionsSetting(globalSetting, localize('settings.codeCompletions.allFiles', "All files"), '*', disposables);532533if (modeId) {534const languageSetting = append(settings, $('div.setting'));535this.createInlineSuggestionsSetting(languageSetting, localize('settings.codeCompletions.language', "{0}", this.languageService.getLanguageName(modeId) ?? modeId), modeId, disposables);536}537}538539// --- Next edit suggestions540{541const setting = append(settings, $('div.setting'));542this.createNextEditSuggestionsSetting(setting, localize('settings.nextEditSuggestions', "Next edit suggestions"), this.getCompletionsSettingAccessor(modeId), disposables);543}544545return settings;546}547548private createSetting(container: HTMLElement, settingIdsToReEvaluate: string[], label: string, accessor: ISettingsAccessor, disposables: DisposableStore): Checkbox {549const checkbox = disposables.add(new Checkbox(label, Boolean(accessor.readSetting()), { ...defaultCheckboxStyles }));550container.appendChild(checkbox.domNode);551552const settingLabel = append(container, $('span.setting-label', undefined, label));553disposables.add(Gesture.addTarget(settingLabel));554[EventType.CLICK, TouchEventType.Tap].forEach(eventType => {555disposables.add(addDisposableListener(settingLabel, eventType, e => {556if (checkbox?.enabled) {557EventHelper.stop(e, true);558559checkbox.checked = !checkbox.checked;560accessor.writeSetting(checkbox.checked);561checkbox.focus();562}563}));564});565566disposables.add(checkbox.onChange(() => {567accessor.writeSetting(checkbox.checked);568}));569570disposables.add(this.configurationService.onDidChangeConfiguration(e => {571if (settingIdsToReEvaluate.some(id => e.affectsConfiguration(id))) {572checkbox.checked = Boolean(accessor.readSetting());573}574}));575576if (!this.canUseChat()) {577container.classList.add('disabled');578checkbox.disable();579checkbox.checked = false;580}581582return checkbox;583}584585private createInlineSuggestionsSetting(container: HTMLElement, label: string, modeId: string | undefined, disposables: DisposableStore): void {586this.createSetting(container, [defaultChat.completionsEnablementSetting], label, this.getCompletionsSettingAccessor(modeId), disposables);587}588589private getCompletionsSettingAccessor(modeId = '*'): ISettingsAccessor {590const settingId = defaultChat.completionsEnablementSetting;591592return {593readSetting: () => isCompletionsEnabled(this.configurationService, modeId),594writeSetting: (value: boolean) => {595this.telemetryService.publicLog2<ChatSettingChangedEvent, ChatSettingChangedClassification>('chatStatus.settingChanged', {596settingIdentifier: settingId,597settingMode: modeId,598settingEnablement: value ? 'enabled' : 'disabled'599});600601let result = this.configurationService.getValue<Record<string, boolean>>(settingId);602if (!isObject(result)) {603result = Object.create(null);604}605606return this.configurationService.updateValue(settingId, { ...result, [modeId]: value });607}608};609}610611private createNextEditSuggestionsSetting(container: HTMLElement, label: string, completionsSettingAccessor: ISettingsAccessor, disposables: DisposableStore): void {612const nesSettingId = defaultChat.nextEditSuggestionsSetting;613const completionsSettingId = defaultChat.completionsEnablementSetting;614const resource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });615616const checkbox = this.createSetting(container, [nesSettingId, completionsSettingId], label, {617readSetting: () => completionsSettingAccessor.readSetting() && this.textResourceConfigurationService.getValue<boolean>(resource, nesSettingId),618writeSetting: (value: boolean) => {619this.telemetryService.publicLog2<ChatSettingChangedEvent, ChatSettingChangedClassification>('chatStatus.settingChanged', {620settingIdentifier: nesSettingId,621settingEnablement: value ? 'enabled' : 'disabled'622});623624return this.textResourceConfigurationService.updateValue(resource, nesSettingId, value);625}626}, disposables);627628// enablement of NES depends on completions setting629// so we have to update our checkbox state accordingly630if (!completionsSettingAccessor.readSetting()) {631container.classList.add('disabled');632checkbox.disable();633}634635disposables.add(this.configurationService.onDidChangeConfiguration(e => {636if (e.affectsConfiguration(completionsSettingId)) {637if (completionsSettingAccessor.readSetting() && this.canUseChat()) {638checkbox.enable();639container.classList.remove('disabled');640} else {641checkbox.disable();642container.classList.add('disabled');643}644}645}));646}647648private createCompletionsSnooze(container: HTMLElement, label: string, disposables: DisposableStore): void {649const isEnabled = () => {650const completionsEnabled = isCompletionsEnabled(this.configurationService);651const completionsEnabledActiveLanguage = isCompletionsEnabled(this.configurationService, this.editorService.activeTextEditorLanguageId);652return completionsEnabled || completionsEnabledActiveLanguage;653};654655const button = disposables.add(new Button(container, { disabled: !isEnabled(), ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate, secondary: true }));656657const timerDisplay = container.appendChild($('span.snooze-label'));658659const actionBar = container.appendChild($('div.snooze-action-bar'));660const toolbar = disposables.add(new ActionBar(actionBar, { hoverDelegate: nativeHoverDelegate }));661const cancelAction = toAction({662id: 'workbench.action.cancelSnoozeStatusBarLink',663label: localize('cancelSnooze', "Cancel Snooze"),664run: () => this.inlineCompletionsService.cancelSnooze(),665class: ThemeIcon.asClassName(Codicon.stopCircle)666});667668const update = (isEnabled: boolean) => {669container.classList.toggle('disabled', !isEnabled);670toolbar.clear();671672const timeLeftMs = this.inlineCompletionsService.snoozeTimeLeft;673if (!isEnabled || timeLeftMs <= 0) {674timerDisplay.textContent = localize('completions.snooze5minutesTitle', "Hide suggestions for 5 min");675timerDisplay.title = '';676button.label = label;677button.setTitle(localize('completions.snooze5minutes', "Hide inline suggestions for 5 min"));678return true;679}680681const timeLeftSeconds = Math.ceil(timeLeftMs / 1000);682const minutes = Math.floor(timeLeftSeconds / 60);683const seconds = timeLeftSeconds % 60;684685timerDisplay.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds} ${localize('completions.remainingTime', "remaining")}`;686timerDisplay.title = localize('completions.snoozeTimeDescription', "Inline suggestions are hidden for the remaining duration");687button.label = localize('completions.plus5min', "+5 min");688button.setTitle(localize('completions.snoozeAdditional5minutes', "Snooze additional 5 min"));689toolbar.push([cancelAction], { icon: true, label: false });690691return false;692};693694// Update every second if there's time remaining695const timerDisposables = disposables.add(new DisposableStore());696function updateIntervalTimer() {697timerDisposables.clear();698const enabled = isEnabled();699700if (update(enabled)) {701return;702}703704timerDisposables.add(disposableWindowInterval(705getWindow(container),706() => update(enabled),7071000708));709}710updateIntervalTimer();711712disposables.add(button.onDidClick(() => {713this.inlineCompletionsService.snooze();714update(isEnabled());715}));716717disposables.add(this.configurationService.onDidChangeConfiguration(e => {718if (e.affectsConfiguration(defaultChat.completionsEnablementSetting)) {719button.enabled = isEnabled();720}721updateIntervalTimer();722}));723724disposables.add(this.inlineCompletionsService.onDidChangeIsSnoozing(e => {725updateIntervalTimer();726}));727}728729private async showModelPicker(provider: languages.InlineCompletionsProvider): Promise<void> {730if (!provider.modelInfo || !provider.setModelId) {731return;732}733734const modelInfo = provider.modelInfo;735const items: IQuickPickItem[] = modelInfo.models.map(model => ({736id: model.id,737label: model.name,738description: model.id === modelInfo.currentModelId ? localize('currentModel.description', "Currently selected") : undefined,739picked: model.id === modelInfo.currentModelId740}));741742const selected = await this.quickInputService.pick(items, {743placeHolder: localize('selectModelFor', "Select a model for {0}", provider.displayName || 'inline completions'),744canPickMany: false745});746747if (selected && selected.id && selected.id !== modelInfo.currentModelId) {748await provider.setModelId(selected.id);749}750751this.hoverService.hideHover(true);752}753}754755756