Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusEntry.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/chatStatus.css';
7
import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';
8
import { localize } from '../../../../../nls.js';
9
import { IWorkbenchContribution } from '../../../../common/contributions.js';
10
import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind } from '../../../../services/statusbar/browser/statusbar.js';
11
import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService, isProUser } from '../../../../services/chat/common/chatEntitlementService.js';
12
import { CancellationToken } from '../../../../../base/common/cancellation.js';
13
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
14
import { IEditorService } from '../../../../services/editor/common/editorService.js';
15
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
16
import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
17
import { IInlineCompletionsService } from '../../../../../editor/browser/services/inlineCompletionsService.js';
18
import { IChatSessionsService } from '../../common/chatSessionsService.js';
19
import { ChatStatusDashboard } from './chatStatusDashboard.js';
20
import { mainWindow } from '../../../../../base/browser/window.js';
21
import { disposableWindowInterval } from '../../../../../base/browser/dom.js';
22
import { isNewUser, isCompletionsEnabled } from './chatStatus.js';
23
import product from '../../../../../platform/product/common/product.js';
24
25
export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution {
26
27
static readonly ID = 'workbench.contrib.chatStatusBarEntry';
28
29
private entry: IStatusbarEntryAccessor | undefined = undefined;
30
31
private readonly activeCodeEditorListener = this._register(new MutableDisposable());
32
33
private runningSessionsCount: number;
34
35
constructor(
36
@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,
37
@IInstantiationService private readonly instantiationService: IInstantiationService,
38
@IStatusbarService private readonly statusbarService: IStatusbarService,
39
@IEditorService private readonly editorService: IEditorService,
40
@IConfigurationService private readonly configurationService: IConfigurationService,
41
@IInlineCompletionsService private readonly completionsService: IInlineCompletionsService,
42
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
43
) {
44
super();
45
46
this.runningSessionsCount = this.chatSessionsService.getInProgress().reduce((total, item) => total + item.count, 0);
47
48
this.update();
49
50
this.registerListeners();
51
}
52
53
private update(): void {
54
const sentiment = this.chatEntitlementService.sentiment;
55
if (!sentiment.hidden) {
56
const props = this.getEntryProps();
57
if (this.entry) {
58
this.entry.update(props);
59
} else {
60
this.entry = this.statusbarService.addEntry(props, 'chat.statusBarEntry', StatusbarAlignment.RIGHT, { location: { id: 'status.editor.mode', priority: 100.1 }, alignment: StatusbarAlignment.RIGHT });
61
}
62
} else {
63
this.entry?.dispose();
64
this.entry = undefined;
65
}
66
}
67
68
private registerListeners(): void {
69
this._register(this.chatEntitlementService.onDidChangeQuotaExceeded(() => this.update()));
70
this._register(this.chatEntitlementService.onDidChangeSentiment(() => this.update()));
71
this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.update()));
72
73
this._register(this.completionsService.onDidChangeIsSnoozing(() => this.update()));
74
75
this._register(this.chatSessionsService.onDidChangeInProgress(() => {
76
const oldSessionsCount = this.runningSessionsCount;
77
this.runningSessionsCount = this.chatSessionsService.getInProgress().reduce((total, item) => total + item.count, 0);
78
if (this.runningSessionsCount !== oldSessionsCount) {
79
this.update();
80
}
81
}));
82
83
this._register(this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange()));
84
85
this._register(this.configurationService.onDidChangeConfiguration(e => {
86
if (e.affectsConfiguration(product.defaultChatAgent?.completionsEnablementSetting)) {
87
this.update();
88
}
89
}));
90
}
91
92
private onDidActiveEditorChange(): void {
93
this.update();
94
95
this.activeCodeEditorListener.clear();
96
97
// Listen to language changes in the active code editor
98
const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorControl);
99
if (activeCodeEditor) {
100
this.activeCodeEditorListener.value = activeCodeEditor.onDidChangeModelLanguage(() => {
101
this.update();
102
});
103
}
104
}
105
106
private getEntryProps(): IStatusbarEntry {
107
let text = '$(copilot)';
108
let ariaLabel = localize('chatStatusAria', "Copilot status");
109
let kind: StatusbarEntryKind | undefined;
110
111
if (isNewUser(this.chatEntitlementService)) {
112
const entitlement = this.chatEntitlementService.entitlement;
113
114
// Finish Setup
115
if (
116
this.chatEntitlementService.sentiment.later || // user skipped setup
117
entitlement === ChatEntitlement.Available || // user is entitled
118
isProUser(entitlement) || // user is already pro
119
entitlement === ChatEntitlement.Free // user is already free
120
) {
121
const finishSetup = localize('finishSetup', "Finish Setup");
122
123
text = `$(copilot) ${finishSetup}`;
124
ariaLabel = finishSetup;
125
kind = 'prominent';
126
}
127
} else {
128
const chatQuotaExceeded = this.chatEntitlementService.quotas.chat?.percentRemaining === 0;
129
const completionsQuotaExceeded = this.chatEntitlementService.quotas.completions?.percentRemaining === 0;
130
131
// Disabled
132
if (this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted) {
133
text = '$(copilot-unavailable)';
134
ariaLabel = localize('copilotDisabledStatus', "Copilot disabled");
135
}
136
137
// Sessions in progress
138
else if (this.runningSessionsCount > 0) {
139
text = '$(copilot-in-progress)';
140
if (this.runningSessionsCount > 1) {
141
ariaLabel = localize('chatSessionsInProgressStatus', "{0} agent sessions in progress", this.runningSessionsCount);
142
} else {
143
ariaLabel = localize('chatSessionInProgressStatus', "1 agent session in progress");
144
}
145
}
146
147
// Signed out
148
else if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) {
149
const signedOutWarning = localize('notSignedIn', "Signed out");
150
151
text = `${this.chatEntitlementService.anonymous ? '$(copilot)' : '$(copilot-not-connected)'} ${signedOutWarning}`;
152
ariaLabel = signedOutWarning;
153
kind = 'prominent';
154
}
155
156
// Free Quota Exceeded
157
else if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && (chatQuotaExceeded || completionsQuotaExceeded)) {
158
let quotaWarning: string;
159
if (chatQuotaExceeded && !completionsQuotaExceeded) {
160
quotaWarning = localize('chatQuotaExceededStatus', "Chat quota reached");
161
} else if (completionsQuotaExceeded && !chatQuotaExceeded) {
162
quotaWarning = localize('completionsQuotaExceededStatus', "Inline suggestions quota reached");
163
} else {
164
quotaWarning = localize('chatAndCompletionsQuotaExceededStatus', "Quota reached");
165
}
166
167
text = `$(copilot-warning) ${quotaWarning}`;
168
ariaLabel = quotaWarning;
169
kind = 'prominent';
170
}
171
172
// Completions Disabled
173
else if (this.editorService.activeTextEditorLanguageId && !isCompletionsEnabled(this.configurationService, this.editorService.activeTextEditorLanguageId)) {
174
text = '$(copilot-unavailable)';
175
ariaLabel = localize('completionsDisabledStatus', "Inline suggestions disabled");
176
}
177
178
// Completions Snoozed
179
else if (this.completionsService.isSnoozing()) {
180
text = '$(copilot-snooze)';
181
ariaLabel = localize('completionsSnoozedStatus', "Inline suggestions snoozed");
182
}
183
}
184
185
const baseResult = {
186
name: localize('chatStatus', "Copilot Status"),
187
text,
188
ariaLabel,
189
command: ShowTooltipCommand,
190
showInAllWindows: true,
191
kind,
192
tooltip: {
193
element: (token: CancellationToken) => {
194
const store = new DisposableStore();
195
store.add(token.onCancellationRequested(() => {
196
store.dispose();
197
}));
198
const elem = ChatStatusDashboard.instantiateInContents(this.instantiationService, store);
199
200
// todo@connor4312/@benibenj: workaround for #257923
201
store.add(disposableWindowInterval(mainWindow, () => {
202
if (!elem.isConnected) {
203
store.dispose();
204
}
205
}, 2000));
206
207
return elem;
208
}
209
}
210
} satisfies IStatusbarEntry;
211
212
return baseResult;
213
}
214
215
override dispose(): void {
216
super.dispose();
217
218
this.entry?.dispose();
219
this.entry = undefined;
220
}
221
}
222
223