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)) { 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(' · '); return parts.map(formatVariantPart).filter(Boolean).join(' · ');
+2 -1
View File
@@ -624,11 +624,12 @@ export async function fetchCodexQuota(
// Extract plan type // Extract plan type
const planTypeRaw = data.plan_type || data.planType; const planTypeRaw = data.plan_type || data.planType;
let planType: 'free' | 'plus' | 'team' | null = null; let planType: 'free' | 'plus' | 'pro' | 'team' | null = null;
if (planTypeRaw) { if (planTypeRaw) {
const normalized = planTypeRaw.toLowerCase(); const normalized = planTypeRaw.toLowerCase();
if (normalized === 'free') planType = 'free'; if (normalized === 'free') planType = 'free';
else if (normalized === 'plus') planType = 'plus'; else if (normalized === 'plus') planType = 'plus';
else if (normalized === 'pro') planType = 'pro';
else if (normalized === 'team') planType = 'team'; else if (normalized === 'team') planType = 'team';
} }
+2 -2
View File
@@ -74,8 +74,8 @@ export interface CodexQuotaResult extends QuotaErrorMetadata {
windows: CodexQuotaWindow[]; windows: CodexQuotaWindow[];
/** Explicit core usage windows (5h + weekly) for easier reset display */ /** Explicit core usage windows (5h + weekly) for easier reset display */
coreUsage?: CodexCoreUsageSummary; coreUsage?: CodexCoreUsageSummary;
/** Plan type: free, plus, team, or null if unknown */ /** Plan type: free, plus, pro, team, or null if unknown */
planType: 'free' | 'plus' | 'team' | null; planType: 'free' | 'plus' | 'pro' | 'team' | null;
/** Timestamp of fetch */ /** Timestamp of fetch */
lastUpdated: number; lastUpdated: number;
/** Error message if fetch failed */ /** Error message if fetch failed */
@@ -100,24 +100,53 @@ function getCodexPlanAudience(quota: unknown): AccountAudience {
if (planType === 'team') return 'business'; if (planType === 'team') return 'business';
if (planType === 'free') return 'free'; if (planType === 'free') return 'free';
if (planType === 'plus') return 'personal'; if (planType === 'plus' || planType === 'pro') return 'personal';
return 'unknown'; return 'unknown';
} }
function getVariantDetailLabel(variant: { function getCodexPlanDetailLabel(quota: unknown): string | null {
audience: AccountAudience; if (!quota || typeof quota !== 'object' || !('planType' in quota)) {
detailLabel?: string | null; return null;
compactDetailLabel?: string | null; }
}) {
return variant.detailLabel ?? variant.compactDetailLabel ?? 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: { function getVariantDetailLabel(
audience: AccountAudience; variant: {
compactDetailLabel?: string | null; audience: AccountAudience;
detailLabel?: string | null; detailLabel?: string | null;
}) { compactDetailLabel?: string | null;
return variant.compactDetailLabel ?? variant.detailLabel ?? 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 { function getDetailedAudienceLabel(audience: AccountAudience): string | null {
@@ -146,7 +175,7 @@ function getVariantInlineLabel(
}, },
quota?: unknown quota?: unknown
) { ) {
const detailLabel = getVariantDetailLabel(variant); const detailLabel = getVariantDetailLabel(variant, quota);
const audienceLabel = const audienceLabel =
variant.audienceLabel ?? getDetailedAudienceLabel(getVariantAudience(variant, quota)); variant.audienceLabel ?? getDetailedAudienceLabel(getVariantAudience(variant, quota));
const composedLabel = [audienceLabel, detailLabel].filter(Boolean).join(' · '); const composedLabel = [audienceLabel, detailLabel].filter(Boolean).join(' · ');
@@ -164,7 +193,7 @@ function getVariantMarkerLabel(
quota?: unknown quota?: unknown
) { ) {
const audience = getVariantAudience(variant, quota); const audience = getVariantAudience(variant, quota);
const compactDetailLabel = getVariantCompactDetailLabel(variant); const compactDetailLabel = getVariantCompactDetailLabel(variant, quota);
if (audience === 'business') { if (audience === 'business') {
const businessVariantCount = audienceCounts.get('business') ?? 0; const businessVariantCount = audienceCounts.get('business') ?? 0;
return businessVariantCount > 1 && compactDetailLabel ? compactDetailLabel : 'Biz'; return businessVariantCount > 1 && compactDetailLabel ? compactDetailLabel : 'Biz';
@@ -197,7 +226,7 @@ function getGroupedVariantSummaryLabel(
const audiences = new Set(variants.map((variant) => variant.audience)); const audiences = new Set(variants.map((variant) => variant.audience));
const hasDistinctDetails = variants.some((variant, index) => const hasDistinctDetails = variants.some((variant, index) =>
Boolean( Boolean(
getVariantCompactDetailLabel(variant) || getVariantCompactDetailLabel(variant, quotas[index]) ||
getDetailedAudienceLabel(getVariantAudience(variant, quotas[index])) getDetailedAudienceLabel(getVariantAudience(variant, quotas[index]))
) )
); );
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import type { UnifiedQuotaResult } from '@/hooks/use-cliproxy-stats'; 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 { cn } from '@/lib/utils';
import { Pause, Star, User } from 'lucide-react'; import { Pause, Star, User } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -109,10 +109,22 @@ function getCodexPlanAudience(quota: UnifiedQuotaResult | undefined): AccountAud
if (quota.planType === 'team') return 'business'; if (quota.planType === 'team') return 'business';
if (quota.planType === 'free') return 'free'; if (quota.planType === 'free') return 'free';
if (quota.planType === 'plus') return 'personal'; if (quota.planType === 'plus' || quota.planType === 'pro') return 'personal';
return 'unknown'; 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({ export function AccountSurfaceCard({
mode, mode,
provider, provider,
@@ -143,11 +155,13 @@ export function AccountSurfaceCard({
const effectiveTier = resolveEffectiveTier(tier, quota); const effectiveTier = resolveEffectiveTier(tier, quota);
const codexPlanAudience = const codexPlanAudience =
normalizedProvider === 'codex' ? getCodexPlanAudience(quota) : 'unknown'; normalizedProvider === 'codex' ? getCodexPlanAudience(quota) : 'unknown';
const codexPlanDetailLabel =
normalizedProvider === 'codex' ? getCodexPlanDetailLabel(quota) : null;
const effectiveAudience = identity.audience !== 'unknown' ? identity.audience : codexPlanAudience; const effectiveAudience = identity.audience !== 'unknown' ? identity.audience : codexPlanAudience;
const effectiveAudienceLabel = const effectiveAudienceLabel =
identity.audienceLabel ?? getDetailedAudienceLabel(effectiveAudience); identity.audienceLabel ?? getDetailedAudienceLabel(effectiveAudience);
const resolvedDetailLabel = identity.detailLabel; const resolvedDetailLabel = identity.detailLabel ?? codexPlanDetailLabel;
const resolvedCompactDetailLabel = identity.compactDetailLabel; const resolvedCompactDetailLabel = identity.compactDetailLabel ?? codexPlanDetailLabel;
const showTierBadge = const showTierBadge =
(normalizedProvider === 'agy' || (normalizedProvider === 'agy' ||
normalizedProvider === 'antigravity' || normalizedProvider === 'antigravity' ||
+5 -3
View File
@@ -186,14 +186,16 @@ export function getAccountIdentityPresentation(
} }
if (suffix && PERSONAL_PLAN_PARTS.has(suffix)) { 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 const inlineLabel = ['Personal', detailLabel].filter(Boolean).join(' · '); // TODO i18n: missing key for Personal
return { return {
email: resolvedEmail, email: resolvedEmail,
audience: 'personal', audience: 'personal',
audienceLabel: 'Personal', audienceLabel: 'Personal',
detailLabel, detailLabel: detailLabel || formatAccountVariantPart(suffix),
compactDetailLabel: detailLabel, compactDetailLabel: detailLabel || formatAccountVariantPart(suffix),
inlineLabel, inlineLabel,
}; };
} }
+2 -2
View File
@@ -610,8 +610,8 @@ export interface CodexQuotaResult {
windows: CodexQuotaWindow[]; windows: CodexQuotaWindow[];
/** Explicit core usage windows (5h + weekly) for easier reset display */ /** Explicit core usage windows (5h + weekly) for easier reset display */
coreUsage?: CodexCoreUsageSummary; coreUsage?: CodexCoreUsageSummary;
/** Plan type: free, plus, team, or null if unknown */ /** Plan type: free, plus, pro, team, or null if unknown */
planType: 'free' | 'plus' | 'team' | null; planType: 'free' | 'plus' | 'pro' | 'team' | null;
/** Timestamp of fetch */ /** Timestamp of fetch */
lastUpdated: number; lastUpdated: number;
/** Upstream HTTP status when available */ /** Upstream HTTP status when available */
@@ -20,7 +20,11 @@ vi.mock('@/hooks/use-cliproxy-stats', async () => {
const mockedUseAccountQuota = vi.mocked(useAccountQuota); const mockedUseAccountQuota = vi.mocked(useAccountQuota);
const mockedUseAccountQuotas = vi.mocked(useAccountQuotas); 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 { return {
success: true, success: true,
planType, planType,
@@ -201,5 +205,6 @@ describe('AccountCard grouped quota tooltip', () => {
expect(screen.getByTitle('Business · Workspace 04a0f049 • Personal · Pro')).toBeInTheDocument(); expect(screen.getByTitle('Business · Workspace 04a0f049 • Personal · Pro')).toBeInTheDocument();
expect(screen.getByText('Personal · Pro')).toBeInTheDocument(); expect(screen.getByText('Personal · Pro')).toBeInTheDocument();
expect(screen.queryByText('Free')).not.toBeInTheDocument(); expect(screen.queryByText('Free')).not.toBeInTheDocument();
expect(screen.getByText('Pro')).toBeInTheDocument();
}); });
}); });
@@ -75,7 +75,7 @@ describe('AccountSurfaceCard', () => {
expect(screen.queryByText('Pers')).not.toBeInTheDocument(); 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( render(
<AccountSurfaceCard <AccountSurfaceCard
mode="compact" mode="compact"
@@ -90,6 +90,7 @@ describe('AccountSurfaceCard', () => {
); );
expect(screen.getByText('Pers')).toBeInTheDocument(); expect(screen.getByText('Pers')).toBeInTheDocument();
expect(screen.getByText('Pro')).toBeInTheDocument();
expect(screen.queryByText('Free')).not.toBeInTheDocument(); expect(screen.queryByText('Free')).not.toBeInTheDocument();
}); });
@@ -109,4 +110,21 @@ describe('AccountSurfaceCard', () => {
expect(screen.getByText('Free')).toBeInTheDocument(); expect(screen.getByText('Free')).toBeInTheDocument();
expect(screen.queryByText('Pers')).not.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)'); ).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( expect(
formatAccountDisplayName( formatAccountDisplayName(
'kaidu.kd@gmail.com', 'kaidu.kd@gmail.com',
'kaidu.kd@gmail.com', 'kaidu.kd@gmail.com',
'codex-kaidu.kd@gmail.com-plus.json' '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', () => { it('leaves plain accounts without inferred state untouched', () => {