Skip to content

Commit f9d4d43

Browse files
osortegaCopilot
andauthored
sessions: move account widget to titlebar with entitlement-aware badges (#307243)
* sessions: move account widget to titlebar with entitlement-aware badges Move the account section from the sidebar footer to the titlebar in the sessions/agents app, providing a more prominent and accessible location for account status and Copilot entitlement information. Key changes: - Add TitleBarAccountWidget with entitlement-aware dot badges (orange for low tokens, red for quota exceeded) - Add TitleBarUpdateWidget positioned between separator and account icon - Create accountTitleBarState.ts pure state machine for badge/icon/kind - Reuse existing ChatStatusDashboard and UpdateHoverWidget in hover panel - Move 'Open in VS Code' to TitleBarSessionMenu (left of separator) - Remove old sidebar footer AccountWidget and its fixture - Add unit tests for the state machine Layout: [session actions] [Open in VS Code] | [Update] [Account] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: handle AvailableForDownload state and stabilize badge key - Add downloadUpdate(true) for the normal AvailableForDownload case (when canInstall !== false), matching the workbench pattern - Remove localized label from badge key to avoid brittleness across locales; key now uses only source, dotBadge, and badge percent - Update unit test assertions for new key format Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent bc37f22 commit f9d4d43

7 files changed

Lines changed: 1183 additions & 399 deletions

File tree

src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts

Lines changed: 477 additions & 234 deletions
Large diffs are not rendered by default.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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 { Codicon } from '../../../../base/common/codicons.js';
7+
import { ThemeIcon } from '../../../../base/common/themables.js';
8+
import { localize } from '../../../../nls.js';
9+
import { ChatEntitlement, IChatSentiment, IQuotaSnapshot } from '../../../../workbench/services/chat/common/chatEntitlementService.js';
10+
11+
export type AccountTitleBarStateSource = 'account' | 'copilot';
12+
export type AccountTitleBarStateKind = 'default' | 'accent' | 'warning' | 'prominent';
13+
14+
export interface IAccountTitleBarStateContext {
15+
readonly isAccountLoading: boolean;
16+
readonly accountName?: string;
17+
readonly accountProviderLabel?: string;
18+
readonly entitlement: ChatEntitlement;
19+
readonly sentiment: Pick<IChatSentiment, 'hidden' | 'disabled' | 'untrusted'>;
20+
readonly quotas: {
21+
readonly chat?: IQuotaSnapshot;
22+
readonly completions?: IQuotaSnapshot;
23+
};
24+
}
25+
26+
export interface IAccountTitleBarState {
27+
readonly source: AccountTitleBarStateSource;
28+
readonly kind: AccountTitleBarStateKind;
29+
readonly icon: ThemeIcon;
30+
readonly label: string;
31+
readonly ariaLabel: string;
32+
readonly badge?: string;
33+
readonly dotBadge?: 'warning' | 'error';
34+
readonly revealLabelOnHover?: boolean;
35+
}
36+
37+
export function getAccountTitleBarBadgeKey(state: IAccountTitleBarState): string | undefined {
38+
if (!state.dotBadge) {
39+
return undefined;
40+
}
41+
42+
return `${state.source}:${state.dotBadge}:${state.badge ?? ''}`;
43+
}
44+
45+
export function getAccountTitleBarState(context: IAccountTitleBarStateContext): IAccountTitleBarState {
46+
if (context.isAccountLoading) {
47+
return {
48+
source: 'account',
49+
kind: 'default',
50+
icon: ThemeIcon.modify(Codicon.loading, 'spin'),
51+
label: localize('loadingAccount', "Loading Account..."),
52+
ariaLabel: localize('loadingAccountAria', "Loading account"),
53+
revealLabelOnHover: true,
54+
};
55+
}
56+
57+
const copilotState = getCopilotPresentation(context.entitlement, context.sentiment, context.quotas);
58+
if (copilotState) {
59+
return copilotState;
60+
}
61+
62+
if (context.accountName) {
63+
return {
64+
source: 'account',
65+
kind: 'default',
66+
icon: Codicon.account,
67+
label: context.accountName,
68+
revealLabelOnHover: true,
69+
ariaLabel: context.accountProviderLabel
70+
? localize('accountSignedInAria', "Signed in as {0} with {1}", context.accountName, context.accountProviderLabel)
71+
: localize('accountSignedInAriaNameOnly', "Signed in as {0}", context.accountName),
72+
};
73+
}
74+
75+
return {
76+
source: 'account',
77+
kind: 'prominent',
78+
icon: Codicon.account,
79+
label: localize('signInLabel', "Sign In"),
80+
ariaLabel: localize('signInAria', "Sign in to your account"),
81+
};
82+
}
83+
84+
function getCopilotPresentation(
85+
entitlement: ChatEntitlement,
86+
sentiment: Pick<IChatSentiment, 'hidden' | 'disabled' | 'untrusted'>,
87+
quotas: { readonly chat?: IQuotaSnapshot; readonly completions?: IQuotaSnapshot }
88+
): IAccountTitleBarState | undefined {
89+
if (sentiment.hidden) {
90+
return undefined;
91+
}
92+
93+
if (entitlement === ChatEntitlement.Unknown) {
94+
return {
95+
source: 'copilot',
96+
kind: 'prominent',
97+
icon: Codicon.account,
98+
label: localize('copilotSignedOut', "Copilot Signed Out"),
99+
ariaLabel: localize('copilotSignedOutAria', "GitHub Copilot is signed out"),
100+
};
101+
}
102+
103+
if (sentiment.disabled || sentiment.untrusted) {
104+
return {
105+
source: 'copilot',
106+
kind: 'warning',
107+
icon: Codicon.account,
108+
label: localize('copilotUnavailable', "Copilot Unavailable"),
109+
ariaLabel: sentiment.untrusted
110+
? localize('copilotUnavailableUntrustedAria', "GitHub Copilot is unavailable in untrusted workspaces")
111+
: localize('copilotUnavailableDisabledAria', "GitHub Copilot is disabled"),
112+
};
113+
}
114+
115+
const chatQuotaExceeded = quotas.chat?.percentRemaining === 0;
116+
const completionsQuotaExceeded = quotas.completions?.percentRemaining === 0;
117+
if (entitlement === ChatEntitlement.Free && (chatQuotaExceeded || completionsQuotaExceeded)) {
118+
return {
119+
source: 'copilot',
120+
kind: 'warning',
121+
icon: Codicon.account,
122+
label: localize('copilotQuotaReached', "Quota Reached"),
123+
dotBadge: 'error',
124+
ariaLabel: getQuotaReachedAriaLabel(chatQuotaExceeded, completionsQuotaExceeded),
125+
};
126+
}
127+
128+
const remainingPercent = getLowestPositivePercent(quotas.chat, quotas.completions);
129+
if (entitlement === ChatEntitlement.Free && typeof remainingPercent === 'number' && remainingPercent <= 25) {
130+
return {
131+
source: 'copilot',
132+
kind: remainingPercent <= 10 ? 'warning' : 'accent',
133+
icon: Codicon.account,
134+
label: localize('copilotTokensRemaining', "Tokens Remaining"),
135+
badge: `${remainingPercent}%`,
136+
dotBadge: remainingPercent <= 10 ? 'error' : 'warning',
137+
ariaLabel: localize('copilotTokensRemainingAria', "{0}% GitHub Copilot tokens remaining", remainingPercent),
138+
};
139+
}
140+
141+
return undefined;
142+
}
143+
144+
function getLowestPositivePercent(...quotas: Array<IQuotaSnapshot | undefined>): number | undefined {
145+
let lowest: number | undefined;
146+
for (const quota of quotas) {
147+
if (typeof quota?.percentRemaining !== 'number' || quota.percentRemaining <= 0) {
148+
continue;
149+
}
150+
151+
lowest = typeof lowest === 'number'
152+
? Math.min(lowest, quota.percentRemaining)
153+
: quota.percentRemaining;
154+
}
155+
156+
return lowest;
157+
}
158+
159+
function getQuotaReachedAriaLabel(chatQuotaExceeded: boolean, completionsQuotaExceeded: boolean): string {
160+
if (chatQuotaExceeded && completionsQuotaExceeded) {
161+
return localize('copilotAllQuotaReachedAria', "GitHub Copilot chat and inline suggestion quota reached");
162+
}
163+
164+
if (chatQuotaExceeded) {
165+
return localize('copilotChatQuotaReachedAria', "GitHub Copilot chat quota reached");
166+
}
167+
168+
return localize('copilotCompletionsQuotaReachedAria', "GitHub Copilot inline suggestion quota reached");
169+
}

0 commit comments

Comments
 (0)