diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 90b8a8a7d95..2aa77bbacb1 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -114,7 +114,7 @@ import { ChatTerminalOutputAccessibleView } from './chatTerminalOutputAccessible import { LocalChatSessionsProvider } from './chatSessions/localChatSessionsProvider.js'; import { ChatSessionsView, ChatSessionsViewContrib } from './chatSessions/view/chatSessionsView.js'; import { ChatSetupContribution, ChatTeardownContribution } from './chatSetup.js'; -import { ChatStatusBarEntry } from './chatStatus/chatStatus.js'; +import { ChatStatusBarEntry } from './chatStatus/chatStatusEntry.js'; import { ChatVariablesService } from './chatVariables.js'; import { ChatWidget } from './chatWidget.js'; import { ChatCodeBlockContextProviderService } from './codeBlockContextProviderService.js'; diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatus.ts index 5dd58a3ae7d..ef3e5989e8c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatus.ts @@ -3,259 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/chatStatus.css'; -import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; -import { localize } from '../../../../../nls.js'; -import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind } from '../../../../services/statusbar/browser/statusbar.js'; -import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService, isProUser } from '../../../../services/chat/common/chatEntitlementService.js'; -import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { ChatEntitlement, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; -import { Color } from '../../../../../base/common/color.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { IInlineCompletionsService } from '../../../../../editor/browser/services/inlineCompletionsService.js'; -import { IChatSessionsService } from '../../common/chatSessionsService.js'; -import { ChatStatusDashboard } from './chatStatusDashboard.js'; -import { mainWindow } from '../../../../../base/browser/window.js'; -import { disposableWindowInterval } from '../../../../../base/browser/dom.js'; -import { defaultChat, isNewUser, isCompletionsEnabled } from './common.js'; +import product from '../../../../../platform/product/common/product.js'; +import { isObject } from '../../../../../base/common/types.js'; -const gaugeForeground = registerColor('gauge.foreground', { - dark: inputValidationInfoBorder, - light: inputValidationInfoBorder, - hcDark: contrastBorder, - hcLight: contrastBorder -}, localize('gaugeForeground', "Gauge foreground color.")); - -registerColor('gauge.background', { - dark: transparent(gaugeForeground, 0.3), - light: transparent(gaugeForeground, 0.3), - hcDark: Color.white, - hcLight: Color.white -}, localize('gaugeBackground', "Gauge background color.")); - -registerColor('gauge.border', { - dark: null, - light: null, - hcDark: contrastBorder, - hcLight: contrastBorder -}, localize('gaugeBorder', "Gauge border color.")); - -const gaugeWarningForeground = registerColor('gauge.warningForeground', { - dark: inputValidationWarningBorder, - light: inputValidationWarningBorder, - hcDark: contrastBorder, - hcLight: contrastBorder -}, localize('gaugeWarningForeground', "Gauge warning foreground color.")); - -registerColor('gauge.warningBackground', { - dark: transparent(gaugeWarningForeground, 0.3), - light: transparent(gaugeWarningForeground, 0.3), - hcDark: Color.white, - hcLight: Color.white -}, localize('gaugeWarningBackground', "Gauge warning background color.")); - -const gaugeErrorForeground = registerColor('gauge.errorForeground', { - dark: inputValidationErrorBorder, - light: inputValidationErrorBorder, - hcDark: contrastBorder, - hcLight: contrastBorder -}, localize('gaugeErrorForeground', "Gauge error foreground color.")); - -registerColor('gauge.errorBackground', { - dark: transparent(gaugeErrorForeground, 0.3), - light: transparent(gaugeErrorForeground, 0.3), - hcDark: Color.white, - hcLight: Color.white -}, localize('gaugeErrorBackground', "Gauge error background color.")); - -//#endregion - -export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'workbench.contrib.chatStatusBarEntry'; - - private entry: IStatusbarEntryAccessor | undefined = undefined; - - private readonly activeCodeEditorListener = this._register(new MutableDisposable()); - - constructor( - @IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IStatusbarService private readonly statusbarService: IStatusbarService, - @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IInlineCompletionsService private readonly completionsService: IInlineCompletionsService, - @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, - ) { - super(); - - this.update(); - this.registerListeners(); - } - - private update(): void { - const sentiment = this.chatEntitlementService.sentiment; - if (!sentiment.hidden) { - const props = this.getEntryProps(); - if (this.entry) { - this.entry.update(props); - } else { - this.entry = this.statusbarService.addEntry(props, 'chat.statusBarEntry', StatusbarAlignment.RIGHT, { location: { id: 'status.editor.mode', priority: 100.1 }, alignment: StatusbarAlignment.RIGHT }); - } - } else { - this.entry?.dispose(); - this.entry = undefined; - } - } - - private registerListeners(): void { - this._register(this.chatEntitlementService.onDidChangeQuotaExceeded(() => this.update())); - this._register(this.chatEntitlementService.onDidChangeSentiment(() => this.update())); - this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.update())); - this._register(this.completionsService.onDidChangeIsSnoozing(() => this.update())); - this._register(this.chatSessionsService.onDidChangeInProgress(() => this.update())); - - this._register(this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange())); - - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(defaultChat.completionsEnablementSetting)) { - this.update(); - } - })); - } - - private onDidActiveEditorChange(): void { - this.update(); - - this.activeCodeEditorListener.clear(); - - // Listen to language changes in the active code editor - const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorControl); - if (activeCodeEditor) { - this.activeCodeEditorListener.value = activeCodeEditor.onDidChangeModelLanguage(() => { - this.update(); - }); - } - } - - private getEntryProps(): IStatusbarEntry { - let text = '$(copilot)'; - let ariaLabel = localize('chatStatusAria', "Copilot status"); - let kind: StatusbarEntryKind | undefined; - - if (isNewUser(this.chatEntitlementService)) { - const entitlement = this.chatEntitlementService.entitlement; - - // Finish Setup - if ( - this.chatEntitlementService.sentiment.later || // user skipped setup - entitlement === ChatEntitlement.Available || // user is entitled - isProUser(entitlement) || // user is already pro - entitlement === ChatEntitlement.Free // user is already free - ) { - const finishSetup = localize('finishSetup', "Finish Setup"); - - text = `$(copilot) ${finishSetup}`; - ariaLabel = finishSetup; - kind = 'prominent'; - } - } else { - const chatQuotaExceeded = this.chatEntitlementService.quotas.chat?.percentRemaining === 0; - const completionsQuotaExceeded = this.chatEntitlementService.quotas.completions?.percentRemaining === 0; - const chatSessionsInProgressCount = this.chatSessionsService.getInProgress().reduce((total, item) => total + item.count, 0); - - // Disabled - if (this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted) { - text = '$(copilot-unavailable)'; - ariaLabel = localize('copilotDisabledStatus', "Copilot disabled"); - } - - // Sessions in progress - else if (chatSessionsInProgressCount > 0) { - text = '$(copilot-in-progress)'; - if (chatSessionsInProgressCount > 1) { - ariaLabel = localize('chatSessionsInProgressStatus', "{0} agent sessions in progress", chatSessionsInProgressCount); - } else { - ariaLabel = localize('chatSessionInProgressStatus', "1 agent session in progress"); - } - } - - // Signed out - else if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { - const signedOutWarning = localize('notSignedIn', "Signed out"); - - text = `${this.chatEntitlementService.anonymous ? '$(copilot)' : '$(copilot-not-connected)'} ${signedOutWarning}`; - ariaLabel = signedOutWarning; - kind = 'prominent'; - } - - // Free Quota Exceeded - else if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && (chatQuotaExceeded || completionsQuotaExceeded)) { - let quotaWarning: string; - if (chatQuotaExceeded && !completionsQuotaExceeded) { - quotaWarning = localize('chatQuotaExceededStatus', "Chat quota reached"); - } else if (completionsQuotaExceeded && !chatQuotaExceeded) { - quotaWarning = localize('completionsQuotaExceededStatus', "Inline suggestions quota reached"); - } else { - quotaWarning = localize('chatAndCompletionsQuotaExceededStatus', "Quota reached"); - } - - text = `$(copilot-warning) ${quotaWarning}`; - ariaLabel = quotaWarning; - kind = 'prominent'; - } - - // Completions Disabled - else if (this.editorService.activeTextEditorLanguageId && !isCompletionsEnabled(this.configurationService, this.editorService.activeTextEditorLanguageId)) { - text = '$(copilot-unavailable)'; - ariaLabel = localize('completionsDisabledStatus', "Inline suggestions disabled"); - } - - // Completions Snoozed - else if (this.completionsService.isSnoozing()) { - text = '$(copilot-snooze)'; - ariaLabel = localize('completionsSnoozedStatus', "Inline suggestions snoozed"); - } - } - - const baseResult = { - name: localize('chatStatus', "Copilot Status"), - text, - ariaLabel, - command: ShowTooltipCommand, - showInAllWindows: true, - kind, - tooltip: { - element: (token: CancellationToken) => { - const store = new DisposableStore(); - store.add(token.onCancellationRequested(() => { - store.dispose(); - })); - const elem = ChatStatusDashboard.instantiateInContents(this.instantiationService, store); - - // todo@connor4312/@benibenj: workaround for #257923 - store.add(disposableWindowInterval(mainWindow, () => { - if (!elem.isConnected) { - store.dispose(); - } - }, 2000)); - - return elem; - } - } - } satisfies IStatusbarEntry; - - return baseResult; - } - - override dispose(): void { - super.dispose(); - - this.entry?.dispose(); - this.entry = undefined; - } +export function isNewUser(chatEntitlementService: IChatEntitlementService): boolean { + return !chatEntitlementService.sentiment.installed || // chat not installed + chatEntitlementService.entitlement === ChatEntitlement.Available; // not yet signed up to chat +} + +export function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean { + const result = configurationService.getValue>(product.defaultChatAgent.completionsEnablementSetting); + if (!isObject(result)) { + return false; + } + + if (typeof result[modeId] !== 'undefined') { + return Boolean(result[modeId]); // go with setting if explicitly defined + } + + return Boolean(result['*']); // fallback to global setting otherwise } diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index d09904c031a..d50afb92529 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -39,8 +39,13 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { LEGACY_AGENT_SESSIONS_VIEW_ID } from '../../common/constants.js'; import { AGENT_SESSIONS_VIEW_ID } from '../agentSessions/agentSessions.js'; -import { defaultChat, canUseChat, isNewUser, isCompletionsEnabled } from './common.js'; +import { isNewUser, isCompletionsEnabled } from './chatStatus.js'; import { IChatStatusItemService, ChatStatusEntry } from './chatStatusItemService.js'; +import product from '../../../../../platform/product/common/product.js'; +import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; +import { Color } from '../../../../../base/common/color.js'; + +const defaultChat = product.defaultChatAgent; interface ISettingsAccessor { readSetting: () => boolean; @@ -59,7 +64,57 @@ type ChatSettingChangedEvent = { settingEnablement: 'enabled' | 'disabled'; }; +const gaugeForeground = registerColor('gauge.foreground', { + dark: inputValidationInfoBorder, + light: inputValidationInfoBorder, + hcDark: contrastBorder, + hcLight: contrastBorder +}, localize('gaugeForeground', "Gauge foreground color.")); + +registerColor('gauge.background', { + dark: transparent(gaugeForeground, 0.3), + light: transparent(gaugeForeground, 0.3), + hcDark: Color.white, + hcLight: Color.white +}, localize('gaugeBackground', "Gauge background color.")); + +registerColor('gauge.border', { + dark: null, + light: null, + hcDark: contrastBorder, + hcLight: contrastBorder +}, localize('gaugeBorder', "Gauge border color.")); + +const gaugeWarningForeground = registerColor('gauge.warningForeground', { + dark: inputValidationWarningBorder, + light: inputValidationWarningBorder, + hcDark: contrastBorder, + hcLight: contrastBorder +}, localize('gaugeWarningForeground', "Gauge warning foreground color.")); + +registerColor('gauge.warningBackground', { + dark: transparent(gaugeWarningForeground, 0.3), + light: transparent(gaugeWarningForeground, 0.3), + hcDark: Color.white, + hcLight: Color.white +}, localize('gaugeWarningBackground', "Gauge warning background color.")); + +const gaugeErrorForeground = registerColor('gauge.errorForeground', { + dark: inputValidationErrorBorder, + light: inputValidationErrorBorder, + hcDark: contrastBorder, + hcLight: contrastBorder +}, localize('gaugeErrorForeground', "Gauge error foreground color.")); + +registerColor('gauge.errorBackground', { + dark: transparent(gaugeErrorForeground, 0.3), + light: transparent(gaugeErrorForeground, 0.3), + hcDark: Color.white, + hcLight: Color.white +}, localize('gaugeErrorBackground', "Gauge error background color.")); + export class ChatStatusDashboard extends DomWidget { + readonly element = $('div.chat-status-bar-entry-tooltip'); private readonly dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' }); @@ -84,10 +139,10 @@ export class ChatStatusDashboard extends DomWidget { ) { super(); - this._render(); + this.render(); } - private _render(): void { + private render(): void { const token = cancelOnDispose(this._store); let needsSeparator = false; @@ -123,7 +178,7 @@ export class ChatStatusDashboard extends DomWidget { } if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && (Number(chatQuota?.percentRemaining) <= 25 || Number(completionsQuota?.percentRemaining) <= 25)) { - const upgradeProButton = this._store.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate, secondary: canUseChat(this.chatEntitlementService) /* use secondary color when chat can still be used */ })); + const upgradeProButton = this._store.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate, secondary: this.canUseChat() /* use secondary color when chat can still be used */ })); upgradeProButton.label = localize('upgradeToCopilotPro', "Upgrade to GitHub Copilot Pro"); this._store.add(upgradeProButton.onDidClick(() => this.runCommandAndClose('workbench.action.chat.upgradePlan'))); } @@ -235,7 +290,7 @@ export class ChatStatusDashboard extends DomWidget { } // Completions Snooze - if (canUseChat(this.chatEntitlementService)) { + if (this.canUseChat()) { const snooze = append(this.element, $('div.snooze-completions')); this.createCompletionsSnooze(snooze, localize('settings.snooze', "Snooze"), this._store); } @@ -295,6 +350,22 @@ export class ChatStatusDashboard extends DomWidget { } } + private canUseChat(): boolean { + if (!this.chatEntitlementService.sentiment.installed || this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted) { + return false; // chat not installed or not enabled + } + + if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown || this.chatEntitlementService.entitlement === ChatEntitlement.Available) { + return this.chatEntitlementService.anonymous; // signed out or not-yet-signed-up users can only use Chat if anonymous access is allowed + } + + if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && this.chatEntitlementService.quotas.chat?.percentRemaining === 0 && this.chatEntitlementService.quotas.completions?.percentRemaining === 0) { + return false; // free user with no quota left + } + + return true; + } + private renderHeader(container: HTMLElement, disposables: DisposableStore, label: string, action?: IAction): void { const header = container.appendChild($('div.header', undefined, label ?? '')); @@ -475,7 +546,7 @@ export class ChatStatusDashboard extends DomWidget { } })); - if (!canUseChat(this.chatEntitlementService)) { + if (!this.canUseChat()) { container.classList.add('disabled'); checkbox.disable(); checkbox.checked = false; @@ -536,7 +607,7 @@ export class ChatStatusDashboard extends DomWidget { disposables.add(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(completionsSettingId)) { - if (completionsSettingAccessor.readSetting() && canUseChat(this.chatEntitlementService)) { + if (completionsSettingAccessor.readSetting() && this.canUseChat()) { checkbox.enable(); container.classList.remove('disabled'); } else { diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusEntry.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusEntry.ts new file mode 100644 index 00000000000..92aa4d86622 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusEntry.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/chatStatus.css'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../nls.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind } from '../../../../services/statusbar/browser/statusbar.js'; +import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService, isProUser } from '../../../../services/chat/common/chatEntitlementService.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { IInlineCompletionsService } from '../../../../../editor/browser/services/inlineCompletionsService.js'; +import { IChatSessionsService } from '../../common/chatSessionsService.js'; +import { ChatStatusDashboard } from './chatStatusDashboard.js'; +import { mainWindow } from '../../../../../base/browser/window.js'; +import { disposableWindowInterval } from '../../../../../base/browser/dom.js'; +import { isNewUser, isCompletionsEnabled } from './chatStatus.js'; +import product from '../../../../../platform/product/common/product.js'; + +export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.chatStatusBarEntry'; + + private entry: IStatusbarEntryAccessor | undefined = undefined; + + private readonly activeCodeEditorListener = this._register(new MutableDisposable()); + + constructor( + @IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IStatusbarService private readonly statusbarService: IStatusbarService, + @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInlineCompletionsService private readonly completionsService: IInlineCompletionsService, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + ) { + super(); + + this.update(); + this.registerListeners(); + } + + private update(): void { + const sentiment = this.chatEntitlementService.sentiment; + if (!sentiment.hidden) { + const props = this.getEntryProps(); + if (this.entry) { + this.entry.update(props); + } else { + this.entry = this.statusbarService.addEntry(props, 'chat.statusBarEntry', StatusbarAlignment.RIGHT, { location: { id: 'status.editor.mode', priority: 100.1 }, alignment: StatusbarAlignment.RIGHT }); + } + } else { + this.entry?.dispose(); + this.entry = undefined; + } + } + + private registerListeners(): void { + this._register(this.chatEntitlementService.onDidChangeQuotaExceeded(() => this.update())); + this._register(this.chatEntitlementService.onDidChangeSentiment(() => this.update())); + this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.update())); + this._register(this.completionsService.onDidChangeIsSnoozing(() => this.update())); + this._register(this.chatSessionsService.onDidChangeInProgress(() => this.update())); + + this._register(this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange())); + + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(product.defaultChatAgent.completionsEnablementSetting)) { + this.update(); + } + })); + } + + private onDidActiveEditorChange(): void { + this.update(); + + this.activeCodeEditorListener.clear(); + + // Listen to language changes in the active code editor + const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorControl); + if (activeCodeEditor) { + this.activeCodeEditorListener.value = activeCodeEditor.onDidChangeModelLanguage(() => { + this.update(); + }); + } + } + + private getEntryProps(): IStatusbarEntry { + let text = '$(copilot)'; + let ariaLabel = localize('chatStatusAria', "Copilot status"); + let kind: StatusbarEntryKind | undefined; + + if (isNewUser(this.chatEntitlementService)) { + const entitlement = this.chatEntitlementService.entitlement; + + // Finish Setup + if ( + this.chatEntitlementService.sentiment.later || // user skipped setup + entitlement === ChatEntitlement.Available || // user is entitled + isProUser(entitlement) || // user is already pro + entitlement === ChatEntitlement.Free // user is already free + ) { + const finishSetup = localize('finishSetup', "Finish Setup"); + + text = `$(copilot) ${finishSetup}`; + ariaLabel = finishSetup; + kind = 'prominent'; + } + } else { + const chatQuotaExceeded = this.chatEntitlementService.quotas.chat?.percentRemaining === 0; + const completionsQuotaExceeded = this.chatEntitlementService.quotas.completions?.percentRemaining === 0; + const chatSessionsInProgressCount = this.chatSessionsService.getInProgress().reduce((total, item) => total + item.count, 0); + + // Disabled + if (this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted) { + text = '$(copilot-unavailable)'; + ariaLabel = localize('copilotDisabledStatus', "Copilot disabled"); + } + + // Sessions in progress + else if (chatSessionsInProgressCount > 0) { + text = '$(copilot-in-progress)'; + if (chatSessionsInProgressCount > 1) { + ariaLabel = localize('chatSessionsInProgressStatus', "{0} agent sessions in progress", chatSessionsInProgressCount); + } else { + ariaLabel = localize('chatSessionInProgressStatus', "1 agent session in progress"); + } + } + + // Signed out + else if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { + const signedOutWarning = localize('notSignedIn', "Signed out"); + + text = `${this.chatEntitlementService.anonymous ? '$(copilot)' : '$(copilot-not-connected)'} ${signedOutWarning}`; + ariaLabel = signedOutWarning; + kind = 'prominent'; + } + + // Free Quota Exceeded + else if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && (chatQuotaExceeded || completionsQuotaExceeded)) { + let quotaWarning: string; + if (chatQuotaExceeded && !completionsQuotaExceeded) { + quotaWarning = localize('chatQuotaExceededStatus', "Chat quota reached"); + } else if (completionsQuotaExceeded && !chatQuotaExceeded) { + quotaWarning = localize('completionsQuotaExceededStatus', "Inline suggestions quota reached"); + } else { + quotaWarning = localize('chatAndCompletionsQuotaExceededStatus', "Quota reached"); + } + + text = `$(copilot-warning) ${quotaWarning}`; + ariaLabel = quotaWarning; + kind = 'prominent'; + } + + // Completions Disabled + else if (this.editorService.activeTextEditorLanguageId && !isCompletionsEnabled(this.configurationService, this.editorService.activeTextEditorLanguageId)) { + text = '$(copilot-unavailable)'; + ariaLabel = localize('completionsDisabledStatus', "Inline suggestions disabled"); + } + + // Completions Snoozed + else if (this.completionsService.isSnoozing()) { + text = '$(copilot-snooze)'; + ariaLabel = localize('completionsSnoozedStatus', "Inline suggestions snoozed"); + } + } + + const baseResult = { + name: localize('chatStatus', "Copilot Status"), + text, + ariaLabel, + command: ShowTooltipCommand, + showInAllWindows: true, + kind, + tooltip: { + element: (token: CancellationToken) => { + const store = new DisposableStore(); + store.add(token.onCancellationRequested(() => { + store.dispose(); + })); + const elem = ChatStatusDashboard.instantiateInContents(this.instantiationService, store); + + // todo@connor4312/@benibenj: workaround for #257923 + store.add(disposableWindowInterval(mainWindow, () => { + if (!elem.isConnected) { + store.dispose(); + } + }, 2000)); + + return elem; + } + } + } satisfies IStatusbarEntry; + + return baseResult; + } + + override dispose(): void { + super.dispose(); + + this.entry?.dispose(); + this.entry = undefined; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusItemService.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusItemService.ts index 720eefe65f8..b87135ccce6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusItemService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusItemService.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from '../../../../../base/common/event.js'; import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; -export const IChatStatusItemService = createDecorator('IChatStatusItemService'); +export const IChatStatusItemService = createDecorator('chatStatusItemService'); export interface IChatStatusItemService { readonly _serviceBrand: undefined; @@ -21,7 +21,6 @@ export interface IChatStatusItemService { getEntries(): Iterable; } - export interface IChatStatusItemChangeEvent { readonly entry: ChatStatusEntry; } @@ -33,7 +32,6 @@ export type ChatStatusEntry = { detail: string | undefined; }; - class ChatStatusItemService implements IChatStatusItemService { readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/common.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/common.ts deleted file mode 100644 index 2676f86f096..00000000000 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/common.ts +++ /dev/null @@ -1,54 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ChatEntitlement, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import product from '../../../../../platform/product/common/product.js'; -import { isObject } from '../../../../../base/common/types.js'; - -export const defaultChat = { - completionsEnablementSetting: product.defaultChatAgent?.completionsEnablementSetting ?? '', - nextEditSuggestionsSetting: product.defaultChatAgent?.nextEditSuggestionsSetting ?? '', - manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', - manageOverageUrl: product.defaultChatAgent?.manageOverageUrl ?? '', - provider: product.defaultChatAgent?.provider ?? { default: { id: '', name: '' }, enterprise: { id: '', name: '' }, apple: { id: '', name: '' }, google: { id: '', name: '' } }, - termsStatementUrl: product.defaultChatAgent?.termsStatementUrl ?? '', - privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '' -}; - - -export function isNewUser(chatEntitlementService: IChatEntitlementService): boolean { - return !chatEntitlementService.sentiment.installed || // chat not installed - chatEntitlementService.entitlement === ChatEntitlement.Available; // not yet signed up to chat -} - -export function canUseChat(chatEntitlementService: IChatEntitlementService): boolean { - if (!chatEntitlementService.sentiment.installed || chatEntitlementService.sentiment.disabled || chatEntitlementService.sentiment.untrusted) { - return false; // chat not installed or not enabled - } - - if (chatEntitlementService.entitlement === ChatEntitlement.Unknown || chatEntitlementService.entitlement === ChatEntitlement.Available) { - return chatEntitlementService.anonymous; // signed out or not-yet-signed-up users can only use Chat if anonymous access is allowed - } - - if (chatEntitlementService.entitlement === ChatEntitlement.Free && chatEntitlementService.quotas.chat?.percentRemaining === 0 && chatEntitlementService.quotas.completions?.percentRemaining === 0) { - return false; // free user with no quota left - } - - return true; -} - -export function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean { - const result = configurationService.getValue>(defaultChat.completionsEnablementSetting); - if (!isObject(result)) { - return false; - } - - if (typeof result[modeId] !== 'undefined') { - return Boolean(result[modeId]); // go with setting if explicitly defined - } - - return Boolean(result['*']); // fallback to global setting otherwise -}