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