diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index b73f611bd53..a8cea2727da 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -9,7 +9,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { fromNowByDay } from '../../../../../base/common/date.js'; import { Event } from '../../../../../base/common/event.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; @@ -32,13 +32,12 @@ import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; import { IHostService } from '../../../../services/host/browser/host.js'; -import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../../services/statusbar/browser/statusbar.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { EXTENSIONS_CATEGORY, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { extractAgentAndCommand } from '../../common/chatParserTypes.js'; -import { IChatQuotasService, OPEN_CHAT_QUOTA_EXCEEDED_DIALOG } from '../chatQuotasService.js'; +import { IChatQuotasService, OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, quotaToButtonMessage } from '../chatQuotasService.js'; import { IChatDetail, IChatService } from '../../common/chatService.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js'; @@ -599,13 +598,13 @@ export class ChatCommandCenterRendering extends Disposable implements IWorkbench } else if (!chatExtensionInstalled) { primaryAction = instantiationService.createInstance(MenuItemAction, { id: 'workbench.action.chat.triggerSetup', - title: localize2('triggerChatSetup', "Use AI Features with Copilot for Free"), + title: localize2('triggerChatSetup', "Use AI Features with Copilot for Free..."), icon: Codicon.copilot, }, undefined, undefined, undefined, undefined); } else { primaryAction = instantiationService.createInstance(MenuItemAction, { id: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, - title: quotaToMessage({ chatQuotaExceeded, completionsQuotaExceeded }), + title: quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }), icon: Codicon.copilotWarning, }, undefined, undefined, undefined, undefined); } @@ -615,49 +614,3 @@ export class ChatCommandCenterRendering extends Disposable implements IWorkbench } } -export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'chat.quotasStatusBarEntry'; - - private readonly _entry = this._register(new MutableDisposable()); - - constructor( - @IStatusbarService private readonly statusbarService: IStatusbarService, - @IChatQuotasService private readonly chatQuotasService: IChatQuotasService - ) { - super(); - - this._register(this.chatQuotasService.onDidChangeQuotas(() => this.updateStatusbarEntry())); - } - - private updateStatusbarEntry(): void { - const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatQuotasService.quotas; - if (chatQuotaExceeded || completionsQuotaExceeded) { - // Some quota exceeded, show indicator - this._entry.value = this.statusbarService.addEntry({ - name: localize('indicator', "Copilot Quota Indicator"), - text: `$(copilot-warning) ${localize('limitReached', "Limit Reached")}`, - ariaLabel: localize('copilotQuotaExceeded', "Copilot Limit Reached"), - command: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, - kind: 'prominent', - showInAllWindows: true, - tooltip: quotaToMessage({ chatQuotaExceeded, completionsQuotaExceeded }), - }, ChatQuotasStatusBarEntry.ID, StatusbarAlignment.RIGHT, { id: 'GitHub.copilot.status', alignment: StatusbarAlignment.RIGHT }); - } else { - // No quota exceeded, remove indicator - if (this._entry.value) { - this._entry.clear(); - } - } - } -} - -function quotaToMessage({ chatQuotaExceeded, completionsQuotaExceeded }: { chatQuotaExceeded: boolean; completionsQuotaExceeded: boolean }): string { - if (chatQuotaExceeded && !completionsQuotaExceeded) { - return localize('chatQuotaExceeded', "You've reached the monthly chat messages limit, click for details"); - } else if (completionsQuotaExceeded && !chatQuotaExceeded) { - return localize('completionsQuotaExceeded', "You've reached the monthly code completions limit, click for details"); - } else { - return localize('chatAndCompletionsQuotaExceeded', "You've reached the limits of the Copilot Free plan, click for details"); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 79b2aa6f361..f9a212f3c99 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -7,7 +7,7 @@ import { timeout } from '../../../../base/common/async.js'; import { MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; -import { isMacintosh } from '../../../../base/common/platform.js'; +import { isMacintosh, isNative } from '../../../../base/common/platform.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { registerEditorFeature } from '../../../../editor/common/editorFeatures.js'; import * as nls from '../../../../nls.js'; @@ -38,7 +38,7 @@ import { ILanguageModelToolsService } from '../common/languageModelToolsService. import { LanguageModelToolsExtensionPointHandler } from '../common/tools/languageModelToolsContribution.js'; import { IVoiceChatService, VoiceChatService } from '../common/voiceChatService.js'; import { PanelChatAccessibilityHelp, QuickChatAccessibilityHelp } from './actions/chatAccessibilityHelp.js'; -import { ChatCommandCenterRendering, ChatQuotasStatusBarEntry, registerChatActions } from './actions/chatActions.js'; +import { ChatCommandCenterRendering, registerChatActions } from './actions/chatActions.js'; import { ACTION_ID_NEW_CHAT, registerNewChatActions } from './actions/chatClearActions.js'; import { registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js'; import { registerChatContextActions } from './actions/chatContextActions.js'; @@ -80,7 +80,8 @@ import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js' import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js'; import { ChatEditorOverlayController } from './chatEditorOverlay.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; -import { ChatQuotasService, IChatQuotasService } from './chatQuotasService.js'; +import { ChatQuotasService, ChatQuotasStatusBarEntry, IChatQuotasService } from './chatQuotasService.js'; +import { ChatSetupContribution } from './chatSetup.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -322,7 +323,11 @@ registerWorkbenchContribution2(ChatEditorSaving.ID, ChatEditorSaving, WorkbenchP registerWorkbenchContribution2(ChatEditorAutoSaveDisabler.ID, ChatEditorAutoSaveDisabler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingStartedContribution, WorkbenchPhase.Eventually); -registerWorkbenchContribution2(ChatQuotasStatusBarEntry.ID, ChatQuotasStatusBarEntry, WorkbenchPhase.Eventually); + +if (isNative) { + registerWorkbenchContribution2(ChatSetupContribution.ID, ChatSetupContribution, WorkbenchPhase.BlockRestore); + registerWorkbenchContribution2(ChatQuotasStatusBarEntry.ID, ChatQuotasStatusBarEntry, WorkbenchPhase.Eventually); +} registerChatActions(); registerChatCopyActions(); diff --git a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts index 29a5463979b..390b8425973 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts @@ -8,7 +8,7 @@ import { safeIntl } from '../../../../base/common/date.js'; import { language } from '../../../../base/common/platform.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { localize, localize2 } from '../../../../nls.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; @@ -16,10 +16,11 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; -import { CHAT_CATEGORY } from './actions/chatActions.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import product from '../../../../platform/product/common/product.js'; import { URI } from '../../../../base/common/uri.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; export const IChatQuotasService = createDecorator('chatQuotasService'); @@ -101,7 +102,6 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService super({ id: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, title: localize('upgradeChat', "Upgrade to Copilot Pro"), - category: CHAT_CATEGORY }); } @@ -188,3 +188,50 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService this._onDidChangeQuotas.fire(); } } + +export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.quotasStatusBarEntry'; + + private readonly _entry = this._register(new MutableDisposable()); + + constructor( + @IStatusbarService private readonly statusbarService: IStatusbarService, + @IChatQuotasService private readonly chatQuotasService: IChatQuotasService + ) { + super(); + + this._register(this.chatQuotasService.onDidChangeQuotas(() => this.updateStatusbarEntry())); + } + + private updateStatusbarEntry(): void { + const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatQuotasService.quotas; + if (chatQuotaExceeded || completionsQuotaExceeded) { + // Some quota exceeded, show indicator + this._entry.value = this.statusbarService.addEntry({ + name: localize('indicator', "Copilot Quota Indicator"), + text: `$(copilot-warning) ${localize('limitReached', "Limit Reached")}`, + ariaLabel: localize('copilotQuotaExceeded', "Copilot Limit Reached"), + command: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, + kind: 'prominent', + showInAllWindows: true, + tooltip: quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }), + }, ChatQuotasStatusBarEntry.ID, StatusbarAlignment.RIGHT, { id: 'GitHub.copilot.status', alignment: StatusbarAlignment.RIGHT }); + } else { + // No quota exceeded, remove indicator + if (this._entry.value) { + this._entry.clear(); + } + } + } +} + +export function quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }: { chatQuotaExceeded: boolean; completionsQuotaExceeded: boolean }): string { + if (chatQuotaExceeded && !completionsQuotaExceeded) { + return localize('chatQuotaExceeded', "You've reached your monthly chat messages limit, click for details"); + } else if (completionsQuotaExceeded && !chatQuotaExceeded) { + return localize('completionsQuotaExceeded', "You've reached your monthly code completions limit, click for details"); + } else { + return localize('chatAndCompletionsQuotaExceeded', "You've reached the limits of your Copilot Free plan, click for details"); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts similarity index 98% rename from src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts rename to src/vs/workbench/contrib/chat/browser/chatSetup.ts index aaf8bed0d69..05018fc3b30 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -39,7 +39,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js'; import { AuthenticationSession, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; @@ -89,8 +89,11 @@ enum ChatEntitlement { //#region Contribution const TRIGGER_SETUP_COMMAND_ID = 'workbench.action.chat.triggerSetup'; +const TRIGGER_SETUP_COMMAND_LABEL = localize2('triggerChatSetup', "Use AI Features with Copilot for Free..."); -class ChatSetupContribution extends Disposable implements IWorkbenchContribution { +export class ChatSetupContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.chat.setup'; private readonly context = this._register(this.instantiationService.createInstance(ChatSetupContext)); private readonly requests = this._register(this.instantiationService.createInstance(ChatSetupRequests, this.context)); @@ -140,13 +143,10 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution class ChatSetupTriggerAction extends Action2 { - static readonly ID = TRIGGER_SETUP_COMMAND_ID; - static readonly TITLE = localize2('triggerChatSetup', "Use AI Features with Copilot for Free..."); - constructor() { super({ - id: ChatSetupTriggerAction.ID, - title: ChatSetupTriggerAction.TITLE, + id: TRIGGER_SETUP_COMMAND_ID, + title: TRIGGER_SETUP_COMMAND_LABEL, category: CHAT_CATEGORY, f1: true, precondition: ContextKeyExpr.and( @@ -221,7 +221,7 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution const { confirmed } = await dialogService.confirm({ message: localize('hideChatSetupConfirm', "Are you sure you want to hide Copilot?"), - detail: localize('hideChatSetupDetail', "You can restore Copilot by running the '{0}' command.", ChatSetupTriggerAction.TITLE.value), + detail: localize('hideChatSetupDetail', "You can restore Copilot by running the '{0}' command.", TRIGGER_SETUP_COMMAND_LABEL.value), primaryButton: localize('hideChatSetupButton', "Hide Copilot") }); @@ -1011,5 +1011,3 @@ function showCopilotView(viewsService: IViewsService): Promise