fix(accounts): refine codex plan badges

This commit is contained in:
Tam Nhu Tran
2026-04-14 21:12:41 -04:00
parent 72ea1fc9d6
commit 25aa8bdb16
10 changed files with 111 additions and 33 deletions
@@ -162,7 +162,9 @@ export function formatAccountVariantLabel(accountId: string, email?: string): st
}
if (suffix && PERSONAL_PLAN_PARTS.has(suffix)) {
return ['Personal', formatAudienceDetail(parts.slice(0, -1))].filter(Boolean).join(' · ');
return ['Personal', formatVariantPart(suffix), formatAudienceDetail(parts.slice(0, -1))]
.filter(Boolean)
.join(' · ');
}
return parts.map(formatVariantPart).filter(Boolean).join(' · ');
+2 -1
View File
@@ -624,11 +624,12 @@ export async function fetchCodexQuota(
// Extract plan type
const planTypeRaw = data.plan_type || data.planType;
let planType: 'free' | 'plus' | 'team' | null = null;
let planType: 'free' | 'plus' | 'pro' | 'team' | null = null;
if (planTypeRaw) {
const normalized = planTypeRaw.toLowerCase();
if (normalized === 'free') planType = 'free';
else if (normalized === 'plus') planType = 'plus';
else if (normalized === 'pro') planType = 'pro';
else if (normalized === 'team') planType = 'team';
}
+2 -2
View File
@@ -74,8 +74,8 @@ export interface CodexQuotaResult extends QuotaErrorMetadata {
windows: CodexQuotaWindow[];
/** Explicit core usage windows (5h + weekly) for easier reset display */
coreUsage?: CodexCoreUsageSummary;
/** Plan type: free, plus, team, or null if unknown */
planType: 'free' | 'plus' | 'team' | null;
/** Plan type: free, plus, pro, team, or null if unknown */
planType: 'free' | 'plus' | 'pro' | 'team' | null;
/** Timestamp of fetch */
lastUpdated: number;
/** Error message if fetch failed */
@@ -100,24 +100,53 @@ function getCodexPlanAudience(quota: unknown): AccountAudience {
if (planType === 'team') return 'business';
if (planType === 'free') return 'free';
if (planType === 'plus') return 'personal';
if (planType === 'plus' || planType === 'pro') return 'personal';
return 'unknown';
}
function getVariantDetailLabel(variant: {
audience: AccountAudience;
detailLabel?: string | null;
compactDetailLabel?: string | null;
}) {
return variant.detailLabel ?? variant.compactDetailLabel ?? null;
function getCodexPlanDetailLabel(quota: unknown): string | null {
if (!quota || typeof quota !== 'object' || !('planType' in quota)) {
return null;
}
const planType = (quota as { planType?: unknown }).planType;
if (typeof planType !== 'string' || planType.trim().length === 0) {
return null;
}
if (planType === 'free' || planType === 'team') {
return null;
}
return planType === 'plus' || planType === 'pro'
? planType[0].toUpperCase() + planType.slice(1)
: null;
}
function getVariantCompactDetailLabel(variant: {
audience: AccountAudience;
compactDetailLabel?: string | null;
detailLabel?: string | null;
}) {
return variant.compactDetailLabel ?? variant.detailLabel ?? null;
function getVariantDetailLabel(
variant: {
audience: AccountAudience;
detailLabel?: string | null;
compactDetailLabel?: string | null;
},
quota?: unknown
) {
return (
variant.detailLabel ?? variant.compactDetailLabel ?? getCodexPlanDetailLabel(quota) ?? null
);
}
function getVariantCompactDetailLabel(
variant: {
audience: AccountAudience;
compactDetailLabel?: string | null;
detailLabel?: string | null;
},
quota?: unknown
) {
return (
variant.compactDetailLabel ?? variant.detailLabel ?? getCodexPlanDetailLabel(quota) ?? null
);
}
function getDetailedAudienceLabel(audience: AccountAudience): string | null {
@@ -146,7 +175,7 @@ function getVariantInlineLabel(
},
quota?: unknown
) {
const detailLabel = getVariantDetailLabel(variant);
const detailLabel = getVariantDetailLabel(variant, quota);
const audienceLabel =
variant.audienceLabel ?? getDetailedAudienceLabel(getVariantAudience(variant, quota));
const composedLabel = [audienceLabel, detailLabel].filter(Boolean).join(' · ');
@@ -164,7 +193,7 @@ function getVariantMarkerLabel(
quota?: unknown
) {
const audience = getVariantAudience(variant, quota);
const compactDetailLabel = getVariantCompactDetailLabel(variant);
const compactDetailLabel = getVariantCompactDetailLabel(variant, quota);
if (audience === 'business') {
const businessVariantCount = audienceCounts.get('business') ?? 0;
return businessVariantCount > 1 && compactDetailLabel ? compactDetailLabel : 'Biz';
@@ -197,7 +226,7 @@ function getGroupedVariantSummaryLabel(
const audiences = new Set(variants.map((variant) => variant.audience));
const hasDistinctDetails = variants.some((variant, index) =>
Boolean(
getVariantCompactDetailLabel(variant) ||
getVariantCompactDetailLabel(variant, quotas[index]) ||
getDetailedAudienceLabel(getVariantAudience(variant, quotas[index]))
)
);
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
import { Badge } from '@/components/ui/badge';
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import type { UnifiedQuotaResult } from '@/hooks/use-cliproxy-stats';
import { getAccountIdentityPresentation } from '@/lib/account-identity';
import { formatAccountVariantPart, getAccountIdentityPresentation } from '@/lib/account-identity';
import { cn } from '@/lib/utils';
import { Pause, Star, User } from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -109,10 +109,22 @@ function getCodexPlanAudience(quota: UnifiedQuotaResult | undefined): AccountAud
if (quota.planType === 'team') return 'business';
if (quota.planType === 'free') return 'free';
if (quota.planType === 'plus') return 'personal';
if (quota.planType === 'plus' || quota.planType === 'pro') return 'personal';
return 'unknown';
}
function getCodexPlanDetailLabel(quota: UnifiedQuotaResult | undefined): string | null {
if (!quota || !('planType' in quota) || !quota.planType) {
return null;
}
if (quota.planType === 'free' || quota.planType === 'team') {
return null;
}
return formatAccountVariantPart(quota.planType);
}
export function AccountSurfaceCard({
mode,
provider,
@@ -143,11 +155,13 @@ export function AccountSurfaceCard({
const effectiveTier = resolveEffectiveTier(tier, quota);
const codexPlanAudience =
normalizedProvider === 'codex' ? getCodexPlanAudience(quota) : 'unknown';
const codexPlanDetailLabel =
normalizedProvider === 'codex' ? getCodexPlanDetailLabel(quota) : null;
const effectiveAudience = identity.audience !== 'unknown' ? identity.audience : codexPlanAudience;
const effectiveAudienceLabel =
identity.audienceLabel ?? getDetailedAudienceLabel(effectiveAudience);
const resolvedDetailLabel = identity.detailLabel;
const resolvedCompactDetailLabel = identity.compactDetailLabel;
const resolvedDetailLabel = identity.detailLabel ?? codexPlanDetailLabel;
const resolvedCompactDetailLabel = identity.compactDetailLabel ?? codexPlanDetailLabel;
const showTierBadge =
(normalizedProvider === 'agy' ||
normalizedProvider === 'antigravity' ||
+5 -3
View File
@@ -186,14 +186,16 @@ export function getAccountIdentityPresentation(
}
if (suffix && PERSONAL_PLAN_PARTS.has(suffix)) {
const detailLabel = formatAudienceDetail(parts.slice(0, -1));
const detailLabel = [formatAccountVariantPart(suffix), formatAudienceDetail(parts.slice(0, -1))]
.filter(Boolean)
.join(' · ');
const inlineLabel = ['Personal', detailLabel].filter(Boolean).join(' · '); // TODO i18n: missing key for Personal
return {
email: resolvedEmail,
audience: 'personal',
audienceLabel: 'Personal',
detailLabel,
compactDetailLabel: detailLabel,
detailLabel: detailLabel || formatAccountVariantPart(suffix),
compactDetailLabel: detailLabel || formatAccountVariantPart(suffix),
inlineLabel,
};
}
+2 -2
View File
@@ -610,8 +610,8 @@ export interface CodexQuotaResult {
windows: CodexQuotaWindow[];
/** Explicit core usage windows (5h + weekly) for easier reset display */
coreUsage?: CodexCoreUsageSummary;
/** Plan type: free, plus, team, or null if unknown */
planType: 'free' | 'plus' | 'team' | null;
/** Plan type: free, plus, pro, team, or null if unknown */
planType: 'free' | 'plus' | 'pro' | 'team' | null;
/** Timestamp of fetch */
lastUpdated: number;
/** Upstream HTTP status when available */
@@ -20,7 +20,11 @@ vi.mock('@/hooks/use-cliproxy-stats', async () => {
const mockedUseAccountQuota = vi.mocked(useAccountQuota);
const mockedUseAccountQuotas = vi.mocked(useAccountQuotas);
function makeCodexQuota(planType: 'free' | 'plus' | 'team', fiveHour: number, weekly: number) {
function makeCodexQuota(
planType: 'free' | 'plus' | 'pro' | 'team',
fiveHour: number,
weekly: number
) {
return {
success: true,
planType,
@@ -201,5 +205,6 @@ describe('AccountCard grouped quota tooltip', () => {
expect(screen.getByTitle('Business · Workspace 04a0f049 • Personal · Pro')).toBeInTheDocument();
expect(screen.getByText('Personal · Pro')).toBeInTheDocument();
expect(screen.queryByText('Free')).not.toBeInTheDocument();
expect(screen.getByText('Pro')).toBeInTheDocument();
});
});
@@ -75,7 +75,7 @@ describe('AccountSurfaceCard', () => {
expect(screen.queryByText('Pers')).not.toBeInTheDocument();
});
it('keeps the simplified personal audience when live quota planType is coarser', () => {
it('keeps token-derived personal detail when live quota planType is coarser', () => {
render(
<AccountSurfaceCard
mode="compact"
@@ -90,6 +90,7 @@ describe('AccountSurfaceCard', () => {
);
expect(screen.getByText('Pers')).toBeInTheDocument();
expect(screen.getByText('Pro')).toBeInTheDocument();
expect(screen.queryByText('Free')).not.toBeInTheDocument();
});
@@ -109,4 +110,21 @@ describe('AccountSurfaceCard', () => {
expect(screen.getByText('Free')).toBeInTheDocument();
expect(screen.queryByText('Pers')).not.toBeInTheDocument();
});
it('falls back to plus or pro detail when Codex quota exposes a paid plan', () => {
render(
<AccountSurfaceCard
mode="compact"
provider="codex"
accountId="user@example.com"
email="user@example.com"
displayEmail="user@example.com"
quota={createCodexQuotaResult({ planType: 'pro' })}
showQuota={false}
/>
);
expect(screen.getByText('Pers')).toBeInTheDocument();
expect(screen.getByText('Pro')).toBeInTheDocument();
});
});
@@ -58,14 +58,21 @@ describe('account identity presentation', () => {
).toBe('kaidu.kd@gmail.com (Free)');
});
it('collapses paid codex personal plans into a single personal audience label', () => {
it('keeps plus and pro codex personal plans distinct', () => {
expect(
formatAccountDisplayName(
'kaidu.kd@gmail.com',
'kaidu.kd@gmail.com',
'codex-kaidu.kd@gmail.com-plus.json'
)
).toBe('kaidu.kd@gmail.com (Personal)');
).toBe('kaidu.kd@gmail.com (Personal · Plus)');
expect(
formatAccountDisplayName(
'kaidu.kd@gmail.com',
'kaidu.kd@gmail.com',
'codex-kaidu.kd@gmail.com-pro.json'
)
).toBe('kaidu.kd@gmail.com (Personal · Pro)');
});
it('leaves plain accounts without inferred state untouched', () => {