Skip to content

Commit 466ebb5

Browse files
authored
chat - indicate quota hit in prominent status (microsoft#235637) (microsoft#235658)
1 parent f7600d4 commit 466ebb5

File tree

4 files changed

+101
-18
lines changed

4 files changed

+101
-18
lines changed

src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,17 @@
205205
background-color: var(--vscode-statusBarItem-prominentBackground);
206206
}
207207

208-
.monaco-workbench .part.statusbar > .items-container > .statusbar-item.prominent-kind a:hover:not(.disabled) {
208+
/**
209+
* Using :not(.compact-right):not(.compact-left) here to improve the visual appearance
210+
* when a prominent item uses `compact: true` with other items. The presence of the
211+
* !important directive for `background-color` otherwise blocks our special hover handling
212+
* code here:
213+
* https://github.com/microsoft/vscode/blob/c2037f152b2bb3119ce1d87f52987776540dd57f/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts#L483-L505
214+
*
215+
* Note: this is currently only done for the prominent kind, but needs to be expanded if
216+
* other kinds use compact feature.
217+
*/
218+
.monaco-workbench .part.statusbar > .items-container > .statusbar-item.prominent-kind:not(.compact-right):not(.compact-left) a:hover:not(.disabled) {
209219
color: var(--vscode-statusBarItem-prominentHoverForeground);
210220
background-color: var(--vscode-statusBarItem-prominentHoverBackground) !important;
211221
}

src/vs/workbench/browser/parts/statusbar/statusbarPart.ts

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55

66
import './media/statusbarpart.css';
77
import { localize } from '../../../../nls.js';
8-
import { Disposable, DisposableStore, dispose, disposeIfDisposable, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
8+
import { Disposable, DisposableStore, disposeIfDisposable, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
99
import { MultiWindowParts, Part } from '../../part.js';
1010
import { EventType as TouchEventType, Gesture, GestureEvent } from '../../../../base/browser/touch.js';
1111
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
12-
import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarStyleOverride, isStatusbarEntryLocation, IStatusbarEntryLocation, isStatusbarEntryPriority, IStatusbarEntryPriority } from '../../../services/statusbar/browser/statusbar.js';
12+
import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarStyleOverride, isStatusbarEntryLocation, IStatusbarEntryLocation, isStatusbarEntryPriority, IStatusbarEntryPriority, IStatusbarEntryOverride } from '../../../services/statusbar/browser/statusbar.js';
1313
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
1414
import { IAction, Separator, toAction } from '../../../../base/common/actions.js';
1515
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
@@ -75,6 +75,12 @@ export interface IStatusbarEntryContainer extends IDisposable {
7575
*/
7676
updateEntryVisibility(id: string, visible: boolean): void;
7777

78+
/**
79+
* Allows to override the appearance of an entry with the provided ID. Only a subset
80+
* of properties is allowed to be overridden.
81+
*/
82+
overrideEntry(id: string, override: IStatusbarEntryOverride): IDisposable;
83+
7884
/**
7985
* Focused the status bar. If one of the status bar entries was focused, focuses it directly.
8086
*/
@@ -134,6 +140,9 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer {
134140
private readonly _onWillDispose = this._register(new Emitter<void>());
135141
readonly onWillDispose = this._onWillDispose.event;
136142

143+
private readonly onDidOverrideEntry = this._register(new Emitter<string>());
144+
private readonly entryOverrides = new Map<string, IStatusbarEntryOverride>();
145+
137146
private leftItemsContainer: HTMLElement | undefined;
138147
private rightItemsContainer: HTMLElement | undefined;
139148

@@ -173,6 +182,28 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer {
173182
this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles()));
174183
}
175184

185+
overrideEntry(id: string, override: IStatusbarEntryOverride): IDisposable {
186+
this.entryOverrides.set(id, override);
187+
this.onDidOverrideEntry.fire(id);
188+
189+
return toDisposable(() => {
190+
const currentOverride = this.entryOverrides.get(id);
191+
if (currentOverride === override) {
192+
this.entryOverrides.delete(id);
193+
this.onDidOverrideEntry.fire(id);
194+
}
195+
});
196+
}
197+
198+
private withEntryOverride(entry: IStatusbarEntry, id: string): IStatusbarEntry {
199+
const override = this.entryOverrides.get(id);
200+
if (override) {
201+
entry = { ...entry, ...override };
202+
}
203+
204+
return entry;
205+
}
206+
176207
addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {
177208
let priority: IStatusbarEntryPriority;
178209
if (isStatusbarEntryPriority(priorityOrLocation)) {
@@ -220,10 +251,11 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer {
220251
}
221252

222253
private doAddEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor {
254+
const disposables = new DisposableStore();
223255

224256
// View model item
225257
const itemContainer = this.doCreateStatusItem(id, alignment);
226-
const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry, this.hoverDelegate);
258+
const item = disposables.add(this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, this.withEntryOverride(entry, id), this.hoverDelegate));
227259

228260
// View model entry
229261
const viewModelEntry: IStatusbarViewModelEntry = new class implements IStatusbarViewModelEntry {
@@ -245,20 +277,32 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer {
245277
this.appendStatusbarEntry(viewModelEntry);
246278
}
247279

248-
return {
280+
let lastEntry = entry;
281+
const accessor: IStatusbarEntryAccessor = {
249282
update: entry => {
250-
item.update(entry);
283+
lastEntry = entry;
284+
item.update(this.withEntryOverride(entry, id));
251285
},
252286
dispose: () => {
253287
const { needsFullRefresh } = this.doAddOrRemoveModelEntry(viewModelEntry, false);
254288
if (needsFullRefresh) {
255289
this.appendStatusbarEntries();
256290
} else {
257291
itemContainer.remove();
292+
this.updateCompactEntries();
258293
}
259-
dispose(item);
294+
disposables.dispose();
260295
}
261296
};
297+
298+
// React to overrides
299+
disposables.add(this.onDidOverrideEntry.event(overrideEntryId => {
300+
if (overrideEntryId === id) {
301+
accessor.update(lastEntry);
302+
}
303+
}));
304+
305+
return accessor;
262306
}
263307

264308
private doCreateStatusItem(id: string, alignment: StatusbarAlignment, ...extraClasses: string[]): HTMLElement {
@@ -790,6 +834,16 @@ export class StatusbarService extends MultiWindowParts<StatusbarPart> implements
790834
}
791835
}
792836

837+
overrideEntry(id: string, override: IStatusbarEntryOverride): IDisposable {
838+
const disposables = new DisposableStore();
839+
840+
for (const part of this.parts) {
841+
disposables.add(part.overrideEntry(id, override));
842+
}
843+
844+
return disposables;
845+
}
846+
793847
focus(preserveEntryFocus?: boolean): void {
794848
this.activePart.focus(preserveEntryFocus);
795849
}
@@ -856,6 +910,10 @@ export class ScopedStatusbarService extends Disposable implements IStatusbarServ
856910
this.statusbarEntryContainer.updateEntryVisibility(id, visible);
857911
}
858912

913+
overrideEntry(id: string, override: IStatusbarEntryOverride): IDisposable {
914+
return this.statusbarEntryContainer.overrideEntry(id, override);
915+
}
916+
859917
focus(preserveEntryFocus?: boolean): void {
860918
this.statusbarEntryContainer.focus(preserveEntryFocus);
861919
}

src/vs/workbench/contrib/chat/browser/chatQuotasService.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { safeIntl } from '../../../../base/common/date.js';
88
import { language } from '../../../../base/common/platform.js';
99
import { Emitter, Event } from '../../../../base/common/event.js';
1010
import { MarkdownString } from '../../../../base/common/htmlContent.js';
11-
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
11+
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
1212
import { localize, localize2 } from '../../../../nls.js';
1313
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
1414
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
@@ -18,7 +18,7 @@ import { createDecorator, ServicesAccessor } from '../../../../platform/instanti
1818
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
1919
import product from '../../../../platform/product/common/product.js';
2020
import { IWorkbenchContribution } from '../../../common/contributions.js';
21-
import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js';
21+
import { IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js';
2222
import { ChatContextKeys } from '../common/chatContextKeys.js';
2323
import { ICommandService } from '../../../../platform/commands/common/commands.js';
2424

@@ -212,7 +212,7 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo
212212

213213
private static readonly COPILOT_STATUS_ID = 'GitHub.copilot.status'; // TODO@bpasero unify into 1 core indicator
214214

215-
private readonly _entry = this._register(new MutableDisposable<IStatusbarEntryAccessor>());
215+
private readonly entry = this._register(new DisposableStore());
216216

217217
constructor(
218218
@IStatusbarService private readonly statusbarService: IStatusbarService,
@@ -221,12 +221,17 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo
221221
super();
222222

223223
this._register(Event.runAndSubscribe(this.chatQuotasService.onDidChangeQuotas, () => this.updateStatusbarEntry()));
224+
this._register(this.statusbarService.onDidChangeEntryVisibility(e => {
225+
if (e.id === ChatQuotasStatusBarEntry.COPILOT_STATUS_ID) {
226+
this.updateStatusbarEntry();
227+
}
228+
}));
224229
}
225230

226231
private updateStatusbarEntry(): void {
227-
const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatQuotasService.quotas;
232+
this.entry.clear();
228233

229-
// Some quota exceeded, show indicator
234+
const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatQuotasService.quotas;
230235
if (chatQuotaExceeded || completionsQuotaExceeded) {
231236
let text: string;
232237
if (chatQuotaExceeded && !completionsQuotaExceeded) {
@@ -242,23 +247,21 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo
242247
text = `$(copilot-warning) ${text}`;
243248
}
244249

245-
this._entry.value = this.statusbarService.addEntry({
250+
this.entry.add(this.statusbarService.addEntry({
246251
name: localize('indicator', "Copilot Limit Indicator"),
247252
text,
248253
ariaLabel: text,
249254
command: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG,
250255
showInAllWindows: true,
256+
kind: 'prominent',
251257
tooltip: quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded })
252258
}, ChatQuotasStatusBarEntry.ID, StatusbarAlignment.RIGHT, {
253259
id: ChatQuotasStatusBarEntry.COPILOT_STATUS_ID,
254260
alignment: StatusbarAlignment.RIGHT,
255261
compact: isCopilotStatusVisible
256-
});
257-
}
262+
}));
258263

259-
// No quota exceeded, remove indicator
260-
else {
261-
this._entry.clear();
264+
this.entry.add(this.statusbarService.overrideEntry(ChatQuotasStatusBarEntry.COPILOT_STATUS_ID, { kind: 'prominent' }));
262265
}
263266
}
264267
}

src/vs/workbench/services/statusbar/browser/statusbar.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,15 @@ export interface IStatusbarEntryAccessor extends IDisposable {
197197
*/
198198
update(properties: IStatusbarEntry): void;
199199
}
200+
201+
/**
202+
* A way to override a status bar entry appearance. Only a subset of
203+
* properties are currently allowed to override.
204+
*/
205+
export interface IStatusbarEntryOverride {
206+
207+
/**
208+
* The kind of status bar entry. This applies different colors to the entry.
209+
*/
210+
readonly kind?: StatusbarEntryKind;
211+
}

0 commit comments

Comments
 (0)