From 1a7a159d91520e05d4ff9fb2bb1c60dd9a0fec28 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 4 Mar 2025 14:19:02 +0100 Subject: [PATCH] chat - better flow from status when reaching quota (#242548) (#242560) --- .../chat/browser/actions/chatActions.ts | 18 ++-- .../contrib/chat/browser/chatStatus.ts | 88 ++++++++----------- .../contrib/chat/browser/media/chatStatus.css | 14 +-- 3 files changed, 53 insertions(+), 67 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 43a4a2646fd..9ea0054f433 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -726,7 +726,13 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben primaryActionIcon = Codicon.copilotNotConnected; } else if (chatQuotaExceeded || completionsQuotaExceeded) { primaryActionId = OPEN_CHAT_QUOTA_EXCEEDED_DIALOG; - primaryActionTitle = quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }); + if (chatQuotaExceeded && !completionsQuotaExceeded) { + primaryActionTitle = localize('chatQuotaExceededButton', "Monthly chat messages limit reached. Click for details."); + } else if (completionsQuotaExceeded && !chatQuotaExceeded) { + primaryActionTitle = localize('completionsQuotaExceededButton', "Monthly code completions limit reached. Click for details."); + } else { + primaryActionTitle = localize('chatAndCompletionsQuotaExceededButton', "Copilot Free plan limit reached. Click for details."); + } primaryActionIcon = Codicon.copilotWarning; } else { primaryActionId = TOGGLE_CHAT_ACTION_ID; @@ -749,13 +755,3 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben markAsSingleton(disposable); } } - -export function quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }: { chatQuotaExceeded: boolean; completionsQuotaExceeded: boolean }): string { - if (chatQuotaExceeded && !completionsQuotaExceeded) { - return localize('chatQuotaExceededButton', "Monthly chat messages limit reached. Click for details."); - } else if (completionsQuotaExceeded && !chatQuotaExceeded) { - return localize('completionsQuotaExceededButton', "Monthly code completions limit reached. Click for details."); - } else { - return localize('chatAndCompletionsQuotaExceededButton', "Copilot Free plan limit reached. Click for details."); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index f50bf3164f3..eae9aa7f39b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -6,19 +6,18 @@ import './media/chatStatus.css'; import { safeIntl } from '../../../../base/common/date.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { language, OS } from '../../../../base/common/platform.js'; +import { language } from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind, TooltipContent } from '../../../services/statusbar/browser/statusbar.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { quotaToButtonMessage, OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, CHAT_SETUP_ACTION_LABEL, TOGGLE_CHAT_ACTION_ID, CHAT_OPEN_ACTION_ID } from './actions/chatActions.js'; +import { OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, CHAT_SETUP_ACTION_LABEL, TOGGLE_CHAT_ACTION_ID, CHAT_OPEN_ACTION_ID } from './actions/chatActions.js'; import { $, addDisposableListener, append, clearNode, EventHelper, EventLike, EventType } from '../../../../base/browser/dom.js'; import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService } from '../common/chatEntitlementService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; -import { defaultCheckboxStyles, defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { Command } from '../../../../editor/common/languages.js'; @@ -38,6 +37,7 @@ import { IProductService } from '../../../../platform/product/common/productServ import { isObject } from '../../../../base/common/types.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; //#region --- colors @@ -160,13 +160,33 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu private getEntryProps(): IStatusbarEntry { let text = '$(copilot)'; let ariaLabel = localize('chatStatus', "Copilot Status"); - let command: string | Command; - let tooltip: TooltipContent; + let command: string | Command = ShowTooltipCommand; + let tooltip: TooltipContent = { element: token => this.dashboard.value.show(token) }; let kind: StatusbarEntryKind | undefined; - // Quota Exceeded const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatEntitlementService.quotas; - if (chatQuotaExceeded || completionsQuotaExceeded) { + + // Copilot Not Installed + if ( + this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.installed.key) === false || + this.chatEntitlementService.entitlement === ChatEntitlement.Available + ) { + tooltip = CHAT_SETUP_ACTION_LABEL.value; + ariaLabel = tooltip; + command = TOGGLE_CHAT_ACTION_ID; + } + + // Signed out + else if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { + const signInWarning = localize('signInToUseCopilot', "Sign in to Use Copilot..."); + text = `$(copilot-warning) ${signInWarning}`; + ariaLabel = signInWarning; + command = ChatStatusBarEntry.SIGN_IN_COMMAND_ID; + kind = 'prominent'; + } + + // Quota Exceeded + else if (chatQuotaExceeded || completionsQuotaExceeded) { let quotaWarning: string; if (chatQuotaExceeded && !completionsQuotaExceeded) { quotaWarning = localize('chatQuotaExceededStatus', "Chat limit reached"); @@ -179,38 +199,9 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu text = `$(copilot-warning) ${quotaWarning}`; ariaLabel = quotaWarning; command = OPEN_CHAT_QUOTA_EXCEEDED_DIALOG; - tooltip = quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }); kind = 'prominent'; } - // Copilot Not Installed - else if ( - this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.installed.key) === false || - this.chatEntitlementService.entitlement === ChatEntitlement.Available - ) { - tooltip = CHAT_SETUP_ACTION_LABEL.value; - command = TOGGLE_CHAT_ACTION_ID; - } - - // Signed out - else if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { - text = '$(copilot-not-connected) Sign In to Use Copilot'; - ariaLabel = localize('signInToUseCopilot', "Sign in to Use Copilot..."); - tooltip = { - element: token => this.dashboard.value.show(token) - }; - command = ChatStatusBarEntry.SIGN_IN_COMMAND_ID; - kind = 'prominent'; - } - - // Any other User - else { - tooltip = { - element: token => this.dashboard.value.show(token) - }; - command = ShowTooltipCommand; - } - return { name: localize('chatStatus', "Copilot Status"), text, @@ -258,7 +249,7 @@ class ChatStatusDashboard extends Disposable { // Quota Indicator if (this.chatEntitlementService.entitlement === ChatEntitlement.Limited) { - const { chatTotal, chatRemaining, completionsTotal, completionsRemaining, quotaResetDate } = this.chatEntitlementService.quotas; + const { chatTotal, chatRemaining, completionsTotal, completionsRemaining, quotaResetDate, chatQuotaExceeded, completionsQuotaExceeded } = this.chatEntitlementService.quotas; this.element.appendChild($('div.header', undefined, localize('usageTitle', "Copilot Free Usage"))); @@ -267,6 +258,12 @@ class ChatStatusDashboard extends Disposable { this.element.appendChild($('div.description', undefined, localize('limitQuota', "Limits will reset on {0}.", this.dateFormatter.value.format(quotaResetDate)))); + if (chatQuotaExceeded || completionsQuotaExceeded) { + const upgradePlanButton = disposables.add(new Button(this.element, { ...defaultButtonStyles })); + upgradePlanButton.label = localize('upgradeToCopilotPro', "Upgrade to Copilot Pro"); + disposables.add(upgradePlanButton.onDidClick(() => this.commandService.executeCommand('workbench.action.chat.upgradePlan'))); + } + (async () => { await this.chatEntitlementService.update(token); if (token.isCancellationRequested) { @@ -430,17 +427,10 @@ class ChatStatusDashboard extends Disposable { }; for (const entry of entries) { - const keys = this.keybindingService.lookupKeybinding(entry.id); - if (!keys) { - continue; - } - - const shortcut = append(shortcuts, $('div.shortcut', { tabIndex: 0, role: 'button', 'aria-label': entry.text })); - - append(shortcut, $('span.shortcut-label', undefined, entry.text)); - - const shortcutKey = disposables.add(new KeybindingLabel(shortcut, OS, { ...defaultKeybindingLabelStyles })); - shortcutKey.set(keys); + const shortcut = append(shortcuts, $('div.shortcut', { tabIndex: 0, role: 'button', 'aria-label': entry.text }, + $('span.shortcut-label', undefined, entry.text), + $('span.shortcut-value', undefined, this.keybindingService.lookupKeybinding(entry.id)?.getLabel() ?? '') + )); disposables.add(Gesture.addTarget(shortcut)); [EventType.CLICK, TouchEventType.Tap].forEach(eventType => { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index aecbe61a4b1..dfa1fc08335 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -25,6 +25,11 @@ color: var(--vscode-descriptionForeground); } +.chat-status-bar-entry-tooltip .monaco-button { + margin-top: 4px; + margin-bottom: 4px; +} + /* Settings */ .chat-status-bar-entry-tooltip .settings { @@ -87,13 +92,8 @@ cursor: pointer; } -.chat-status-bar-entry-tooltip .shortcuts .shortcut .monaco-keybinding { - cursor: pointer; -} - -.chat-status-bar-entry-tooltip .shortcuts .shortcut .monaco-keybinding > .monaco-keybinding-key { - padding: 2px 4px; - font-size: 10px; +.chat-status-bar-entry-tooltip .shortcuts .shortcut .shortcut-value { + color: var(--vscode-descriptionForeground); } /* Quota Indicator */