Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/browser/accountTitleBarState.ts
13389 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 { 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
import { IDefaultAccountService } from '../../platform/defaultAccount/common/defaultAccount.js';
11
import { IAuthenticationService } from '../../workbench/services/authentication/common/authentication.js';
12
13
export interface IResolvedAccountInfo {
14
readonly accountName: string;
15
readonly accountProviderId: string;
16
readonly accountProviderLabel: string;
17
}
18
19
/**
20
* Resolves the current account info by trying the default account service
21
* first, then falling back to raw GitHub sessions from the authentication
22
* service. The fallback covers the window between session creation and
23
* {@link IDefaultAccountService} initialization.
24
*/
25
export async function resolveAccountInfo(
26
defaultAccountService: IDefaultAccountService,
27
authenticationService: IAuthenticationService,
28
): Promise<IResolvedAccountInfo | undefined> {
29
const account = await defaultAccountService.getDefaultAccount();
30
if (account) {
31
return {
32
accountName: account.accountName,
33
accountProviderId: account.authenticationProvider.id,
34
accountProviderLabel: account.authenticationProvider.name,
35
};
36
}
37
38
try {
39
const sessions = await authenticationService.getSessions('github');
40
if (sessions.length > 0) {
41
return {
42
accountName: sessions[0].account.label,
43
accountProviderId: 'github',
44
accountProviderLabel: 'GitHub',
45
};
46
}
47
} catch {
48
// Provider not available yet
49
}
50
51
return undefined;
52
}
53
54
export type AccountTitleBarStateSource = 'account' | 'copilot';
55
export type AccountTitleBarStateKind = 'default' | 'accent' | 'warning' | 'prominent';
56
57
export interface IAccountTitleBarStateContext {
58
readonly isAccountLoading: boolean;
59
readonly accountName?: string;
60
readonly accountProviderLabel?: string;
61
readonly entitlement: ChatEntitlement;
62
readonly sentiment: IChatSentiment;
63
readonly quotas: {
64
readonly chat?: IQuotaSnapshot;
65
readonly completions?: IQuotaSnapshot;
66
};
67
}
68
69
export interface IAccountTitleBarState {
70
readonly source: AccountTitleBarStateSource;
71
readonly kind: AccountTitleBarStateKind;
72
readonly icon: ThemeIcon;
73
readonly label: string;
74
readonly ariaLabel: string;
75
readonly badge?: string;
76
readonly dotBadge?: 'warning' | 'error';
77
readonly revealLabelOnHover?: boolean;
78
}
79
80
export function getAccountProfileImageUrl(accountProviderId: string | undefined, accountName: string | undefined): string | undefined {
81
if (accountProviderId !== 'github' || !accountName?.trim()) {
82
return undefined;
83
}
84
85
return `https://github.com/${encodeURIComponent(accountName.trim())}.png?size=64`;
86
}
87
88
export function getAccountTitleBarBadgeKey(state: IAccountTitleBarState): string | undefined {
89
if (!state.dotBadge) {
90
return undefined;
91
}
92
93
return `${state.source}:${state.dotBadge}:${state.badge ?? ''}`;
94
}
95
96
export function getAccountTitleBarState(context: IAccountTitleBarStateContext): IAccountTitleBarState {
97
if (context.isAccountLoading) {
98
return {
99
source: 'account',
100
kind: 'default',
101
icon: ThemeIcon.modify(Codicon.loading, 'spin'),
102
label: localize('loadingAccount', "Loading Account..."),
103
ariaLabel: localize('loadingAccountAria', "Loading account"),
104
revealLabelOnHover: true,
105
};
106
}
107
108
const copilotState = getCopilotPresentation(context.entitlement, context.sentiment, context.quotas);
109
if (copilotState) {
110
return copilotState;
111
}
112
113
if (context.accountName) {
114
return {
115
source: 'account',
116
kind: 'default',
117
icon: Codicon.account,
118
label: context.accountName,
119
revealLabelOnHover: true,
120
ariaLabel: context.accountProviderLabel
121
? localize('accountSignedInAria', "Signed in as {0} with {1}", context.accountName, context.accountProviderLabel)
122
: localize('accountSignedInAriaNameOnly', "Signed in as {0}", context.accountName),
123
};
124
}
125
126
return {
127
source: 'account',
128
kind: 'prominent',
129
icon: Codicon.account,
130
label: localize('signInLabel', "Sign In"),
131
ariaLabel: localize('signInAria', "Sign in to your account"),
132
};
133
}
134
135
function getCopilotPresentation(
136
entitlement: ChatEntitlement,
137
sentiment: IChatSentiment,
138
quotas: { readonly chat?: IQuotaSnapshot; readonly completions?: IQuotaSnapshot }
139
): IAccountTitleBarState | undefined {
140
if (sentiment.hidden) {
141
return undefined;
142
}
143
144
if (entitlement === ChatEntitlement.Unknown) {
145
return {
146
source: 'copilot',
147
kind: 'prominent',
148
icon: Codicon.account,
149
label: localize('agentsSignedOut', "Agents Signed Out"),
150
ariaLabel: localize('agentsSignedOutAria', "Agents is signed out"),
151
};
152
}
153
154
if (sentiment.disabled || sentiment.untrusted) {
155
return {
156
source: 'copilot',
157
kind: 'warning',
158
icon: Codicon.account,
159
label: localize('copilotUnavailable', "Copilot Unavailable"),
160
ariaLabel: sentiment.untrusted
161
? localize('copilotUnavailableUntrustedAria', "GitHub Copilot is unavailable in untrusted workspaces")
162
: localize('copilotUnavailableDisabledAria', "GitHub Copilot is disabled"),
163
};
164
}
165
166
const chatQuotaExceeded = quotas.chat?.percentRemaining === 0;
167
const completionsQuotaExceeded = quotas.completions?.percentRemaining === 0;
168
if (entitlement === ChatEntitlement.Free && (chatQuotaExceeded || completionsQuotaExceeded)) {
169
return {
170
source: 'copilot',
171
kind: 'warning',
172
icon: Codicon.account,
173
label: localize('copilotQuotaReached', "Quota Reached"),
174
dotBadge: 'error',
175
ariaLabel: getQuotaReachedAriaLabel(chatQuotaExceeded, completionsQuotaExceeded),
176
};
177
}
178
179
const remainingPercent = getLowestPositivePercent(quotas.chat, quotas.completions);
180
if (entitlement === ChatEntitlement.Free && typeof remainingPercent === 'number' && remainingPercent <= 25) {
181
return {
182
source: 'copilot',
183
kind: remainingPercent <= 10 ? 'warning' : 'accent',
184
icon: Codicon.account,
185
label: localize('copilotTokensRemaining', "Tokens Remaining"),
186
badge: `${remainingPercent}%`,
187
dotBadge: remainingPercent <= 10 ? 'error' : 'warning',
188
ariaLabel: localize('copilotTokensRemainingAria', "{0}% GitHub Copilot tokens remaining", remainingPercent),
189
};
190
}
191
192
return undefined;
193
}
194
195
function getLowestPositivePercent(...quotas: Array<IQuotaSnapshot | undefined>): number | undefined {
196
let lowest: number | undefined;
197
for (const quota of quotas) {
198
if (typeof quota?.percentRemaining !== 'number' || quota.percentRemaining <= 0) {
199
continue;
200
}
201
202
lowest = typeof lowest === 'number'
203
? Math.min(lowest, quota.percentRemaining)
204
: quota.percentRemaining;
205
}
206
207
return lowest;
208
}
209
210
function getQuotaReachedAriaLabel(chatQuotaExceeded: boolean, completionsQuotaExceeded: boolean): string {
211
if (chatQuotaExceeded && completionsQuotaExceeded) {
212
return localize('copilotAllQuotaReachedAria', "GitHub Copilot chat and inline suggestion quota reached");
213
}
214
215
if (chatQuotaExceeded) {
216
return localize('copilotChatQuotaReachedAria', "GitHub Copilot chat quota reached");
217
}
218
219
return localize('copilotCompletionsQuotaReachedAria', "GitHub Copilot inline suggestion quota reached");
220
}
221
222