Path: blob/main/src/vs/sessions/browser/accountTitleBarState.ts
13389 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Codicon } from '../../base/common/codicons.js';6import { ThemeIcon } from '../../base/common/themables.js';7import { localize } from '../../nls.js';8import { ChatEntitlement, IChatSentiment, IQuotaSnapshot } from '../../workbench/services/chat/common/chatEntitlementService.js';9import { IDefaultAccountService } from '../../platform/defaultAccount/common/defaultAccount.js';10import { IAuthenticationService } from '../../workbench/services/authentication/common/authentication.js';1112export interface IResolvedAccountInfo {13readonly accountName: string;14readonly accountProviderId: string;15readonly accountProviderLabel: string;16}1718/**19* Resolves the current account info by trying the default account service20* first, then falling back to raw GitHub sessions from the authentication21* service. The fallback covers the window between session creation and22* {@link IDefaultAccountService} initialization.23*/24export async function resolveAccountInfo(25defaultAccountService: IDefaultAccountService,26authenticationService: IAuthenticationService,27): Promise<IResolvedAccountInfo | undefined> {28const account = await defaultAccountService.getDefaultAccount();29if (account) {30return {31accountName: account.accountName,32accountProviderId: account.authenticationProvider.id,33accountProviderLabel: account.authenticationProvider.name,34};35}3637try {38const sessions = await authenticationService.getSessions('github');39if (sessions.length > 0) {40return {41accountName: sessions[0].account.label,42accountProviderId: 'github',43accountProviderLabel: 'GitHub',44};45}46} catch {47// Provider not available yet48}4950return undefined;51}5253export type AccountTitleBarStateSource = 'account' | 'copilot';54export type AccountTitleBarStateKind = 'default' | 'accent' | 'warning' | 'prominent';5556export interface IAccountTitleBarStateContext {57readonly isAccountLoading: boolean;58readonly accountName?: string;59readonly accountProviderLabel?: string;60readonly entitlement: ChatEntitlement;61readonly sentiment: IChatSentiment;62readonly quotas: {63readonly chat?: IQuotaSnapshot;64readonly completions?: IQuotaSnapshot;65};66}6768export interface IAccountTitleBarState {69readonly source: AccountTitleBarStateSource;70readonly kind: AccountTitleBarStateKind;71readonly icon: ThemeIcon;72readonly label: string;73readonly ariaLabel: string;74readonly badge?: string;75readonly dotBadge?: 'warning' | 'error';76readonly revealLabelOnHover?: boolean;77}7879export function getAccountProfileImageUrl(accountProviderId: string | undefined, accountName: string | undefined): string | undefined {80if (accountProviderId !== 'github' || !accountName?.trim()) {81return undefined;82}8384return `https://github.com/${encodeURIComponent(accountName.trim())}.png?size=64`;85}8687export function getAccountTitleBarBadgeKey(state: IAccountTitleBarState): string | undefined {88if (!state.dotBadge) {89return undefined;90}9192return `${state.source}:${state.dotBadge}:${state.badge ?? ''}`;93}9495export function getAccountTitleBarState(context: IAccountTitleBarStateContext): IAccountTitleBarState {96if (context.isAccountLoading) {97return {98source: 'account',99kind: 'default',100icon: ThemeIcon.modify(Codicon.loading, 'spin'),101label: localize('loadingAccount', "Loading Account..."),102ariaLabel: localize('loadingAccountAria', "Loading account"),103revealLabelOnHover: true,104};105}106107const copilotState = getCopilotPresentation(context.entitlement, context.sentiment, context.quotas);108if (copilotState) {109return copilotState;110}111112if (context.accountName) {113return {114source: 'account',115kind: 'default',116icon: Codicon.account,117label: context.accountName,118revealLabelOnHover: true,119ariaLabel: context.accountProviderLabel120? localize('accountSignedInAria', "Signed in as {0} with {1}", context.accountName, context.accountProviderLabel)121: localize('accountSignedInAriaNameOnly', "Signed in as {0}", context.accountName),122};123}124125return {126source: 'account',127kind: 'prominent',128icon: Codicon.account,129label: localize('signInLabel', "Sign In"),130ariaLabel: localize('signInAria', "Sign in to your account"),131};132}133134function getCopilotPresentation(135entitlement: ChatEntitlement,136sentiment: IChatSentiment,137quotas: { readonly chat?: IQuotaSnapshot; readonly completions?: IQuotaSnapshot }138): IAccountTitleBarState | undefined {139if (sentiment.hidden) {140return undefined;141}142143if (entitlement === ChatEntitlement.Unknown) {144return {145source: 'copilot',146kind: 'prominent',147icon: Codicon.account,148label: localize('agentsSignedOut', "Agents Signed Out"),149ariaLabel: localize('agentsSignedOutAria', "Agents is signed out"),150};151}152153if (sentiment.disabled || sentiment.untrusted) {154return {155source: 'copilot',156kind: 'warning',157icon: Codicon.account,158label: localize('copilotUnavailable', "Copilot Unavailable"),159ariaLabel: sentiment.untrusted160? localize('copilotUnavailableUntrustedAria', "GitHub Copilot is unavailable in untrusted workspaces")161: localize('copilotUnavailableDisabledAria', "GitHub Copilot is disabled"),162};163}164165const chatQuotaExceeded = quotas.chat?.percentRemaining === 0;166const completionsQuotaExceeded = quotas.completions?.percentRemaining === 0;167if (entitlement === ChatEntitlement.Free && (chatQuotaExceeded || completionsQuotaExceeded)) {168return {169source: 'copilot',170kind: 'warning',171icon: Codicon.account,172label: localize('copilotQuotaReached', "Quota Reached"),173dotBadge: 'error',174ariaLabel: getQuotaReachedAriaLabel(chatQuotaExceeded, completionsQuotaExceeded),175};176}177178const remainingPercent = getLowestPositivePercent(quotas.chat, quotas.completions);179if (entitlement === ChatEntitlement.Free && typeof remainingPercent === 'number' && remainingPercent <= 25) {180return {181source: 'copilot',182kind: remainingPercent <= 10 ? 'warning' : 'accent',183icon: Codicon.account,184label: localize('copilotTokensRemaining', "Tokens Remaining"),185badge: `${remainingPercent}%`,186dotBadge: remainingPercent <= 10 ? 'error' : 'warning',187ariaLabel: localize('copilotTokensRemainingAria', "{0}% GitHub Copilot tokens remaining", remainingPercent),188};189}190191return undefined;192}193194function getLowestPositivePercent(...quotas: Array<IQuotaSnapshot | undefined>): number | undefined {195let lowest: number | undefined;196for (const quota of quotas) {197if (typeof quota?.percentRemaining !== 'number' || quota.percentRemaining <= 0) {198continue;199}200201lowest = typeof lowest === 'number'202? Math.min(lowest, quota.percentRemaining)203: quota.percentRemaining;204}205206return lowest;207}208209function getQuotaReachedAriaLabel(chatQuotaExceeded: boolean, completionsQuotaExceeded: boolean): string {210if (chatQuotaExceeded && completionsQuotaExceeded) {211return localize('copilotAllQuotaReachedAria', "GitHub Copilot chat and inline suggestion quota reached");212}213214if (chatQuotaExceeded) {215return localize('copilotChatQuotaReachedAria', "GitHub Copilot chat quota reached");216}217218return localize('copilotCompletionsQuotaReachedAria', "GitHub Copilot inline suggestion quota reached");219}220221222