mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-21 10:17:25 +00:00
chat - polish chat status after move (#279140)
* chat - polish chat status after move * .
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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<Record<string, boolean>>(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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>('IChatStatusItemService');
|
||||
export const IChatStatusItemService = createDecorator<IChatStatusItemService>('chatStatusItemService');
|
||||
|
||||
export interface IChatStatusItemService {
|
||||
readonly _serviceBrand: undefined;
|
||||
@@ -21,7 +21,6 @@ export interface IChatStatusItemService {
|
||||
getEntries(): Iterable<ChatStatusEntry>;
|
||||
}
|
||||
|
||||
|
||||
export interface IChatStatusItemChangeEvent {
|
||||
readonly entry: ChatStatusEntry;
|
||||
}
|
||||
@@ -33,7 +32,6 @@ export type ChatStatusEntry = {
|
||||
detail: string | undefined;
|
||||
};
|
||||
|
||||
|
||||
class ChatStatusItemService implements IChatStatusItemService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
|
||||
@@ -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<Record<string, boolean>>(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
|
||||
}
|
||||
Reference in New Issue
Block a user