fix(accounts): simplify codex free tier badges

This commit is contained in:
Tam Nhu Tran
2026-04-14 21:01:48 -04:00
parent 154c7d0e9c
commit 72ea1fc9d6
11 changed files with 227 additions and 96 deletions
@@ -1,6 +1,9 @@
import type { CLIProxyProvider } from '../types'; import type { CLIProxyProvider } from '../types';
const DUPLICATE_EMAIL_ACCOUNT_PROVIDERS = new Set<string>(['codex']); const DUPLICATE_EMAIL_ACCOUNT_PROVIDERS = new Set<string>(['codex']);
const FREE_PLAN_PARTS = new Set(['free']);
const PERSONAL_PLAN_PARTS = new Set(['plus', 'pro']);
const BUSINESS_PLAN_PARTS = new Set(['team']);
// Keep variant parsing aligned with ui/src/lib/account-identity.ts. The UI copy is // Keep variant parsing aligned with ui/src/lib/account-identity.ts. The UI copy is
// separate because the browser bundle cannot import this server module directly. // separate because the browser bundle cannot import this server module directly.
@@ -49,6 +52,20 @@ function formatVariantPart(value: string): string {
} }
} }
function formatWorkspaceLabel(parts: string[]): string | null {
const workspaceId = parts.find((part) => /^[a-f0-9]{8}$/i.test(part));
if (workspaceId) {
return `Workspace ${workspaceId.toLowerCase()}`;
}
return parts.map(formatVariantPart).filter(Boolean).join(' · ') || null;
}
function formatAudienceDetail(parts: string[]): string | null {
const label = parts.map(formatVariantPart).filter(Boolean).join(' · ');
return label || null;
}
export function supportsDuplicateEmailAccounts(provider: CLIProxyProvider | string): boolean { export function supportsDuplicateEmailAccounts(provider: CLIProxyProvider | string): boolean {
return DUPLICATE_EMAIL_ACCOUNT_PROVIDERS.has(normalizeProvider(provider)); return DUPLICATE_EMAIL_ACCOUNT_PROVIDERS.has(normalizeProvider(provider));
} }
@@ -136,10 +153,16 @@ export function formatAccountVariantLabel(accountId: string, email?: string): st
} }
const suffix = parts[parts.length - 1]?.toLowerCase(); const suffix = parts[parts.length - 1]?.toLowerCase();
if (suffix && ['team', 'free', 'plus', 'pro'].includes(suffix)) { if (suffix && BUSINESS_PLAN_PARTS.has(suffix)) {
return [formatVariantPart(suffix), ...parts.slice(0, -1).map(formatVariantPart)] return ['Business', formatWorkspaceLabel(parts.slice(0, -1))].filter(Boolean).join(' · ');
.filter(Boolean) }
.join(' · ');
if (suffix && FREE_PLAN_PARTS.has(suffix)) {
return ['Free', formatAudienceDetail(parts.slice(0, -1))].filter(Boolean).join(' · ');
}
if (suffix && PERSONAL_PLAN_PARTS.has(suffix)) {
return ['Personal', formatAudienceDetail(parts.slice(0, -1))].filter(Boolean).join(' · ');
} }
return parts.map(formatVariantPart).filter(Boolean).join(' · '); return parts.map(formatVariantPart).filter(Boolean).join(' · ');
@@ -13,7 +13,6 @@ import {
getProviderResetTime, getProviderResetTime,
getQuotaFailureInfo, getQuotaFailureInfo,
} from '@/lib/utils'; } from '@/lib/utils';
import { formatAccountVariantPart } from '@/lib/account-identity';
import { GripVertical, Loader2, Pause, Play } from 'lucide-react'; import { GripVertical, Loader2, Pause, Play } from 'lucide-react';
import { import {
useAccountQuota, useAccountQuota,
@@ -28,6 +27,7 @@ import { AccountCardStats } from './account-card-stats';
import { cleanEmail } from './utils'; import { cleanEmail } from './utils';
type Zone = 'left' | 'right' | 'top' | 'bottom'; type Zone = 'left' | 'right' | 'top' | 'bottom';
type AccountAudience = 'business' | 'free' | 'personal' | 'unknown';
const QUOTA_PROVIDER_ALIASES = [ const QUOTA_PROVIDER_ALIASES = [
'antigravity', 'antigravity',
@@ -88,46 +88,57 @@ function getCompactQuotaColor(percentage: number) {
return 'bg-red-500'; return 'bg-red-500';
} }
function getCodexPlanDetailLabel(quota: unknown): string | null { function getCodexPlanAudience(quota: unknown): AccountAudience {
if (!quota || typeof quota !== 'object' || !('planType' in quota)) { if (!quota || typeof quota !== 'object' || !('planType' in quota)) {
return null; return 'unknown';
} }
const planType = (quota as { planType?: unknown }).planType; const planType = (quota as { planType?: unknown }).planType;
if (typeof planType !== 'string' || planType.trim().length === 0) { if (typeof planType !== 'string' || planType.trim().length === 0) {
return null; return 'unknown';
} }
return formatAccountVariantPart(planType); if (planType === 'team') return 'business';
if (planType === 'free') return 'free';
if (planType === 'plus') return 'personal';
return 'unknown';
} }
function getVariantDetailLabel( function getVariantDetailLabel(variant: {
audience: AccountAudience;
detailLabel?: string | null;
compactDetailLabel?: string | null;
}) {
return variant.detailLabel ?? variant.compactDetailLabel ?? null;
}
function getVariantCompactDetailLabel(variant: {
audience: AccountAudience;
compactDetailLabel?: string | null;
detailLabel?: string | null;
}) {
return variant.compactDetailLabel ?? variant.detailLabel ?? null;
}
function getDetailedAudienceLabel(audience: AccountAudience): string | null {
if (audience === 'business') return 'Business';
if (audience === 'free') return 'Free';
if (audience === 'personal') return 'Personal';
return null;
}
function getVariantAudience(
variant: { variant: {
audience: string; audience: AccountAudience;
detailLabel?: string | null;
compactDetailLabel?: string | null;
}, },
quota?: unknown quota?: unknown
) { ) {
const codexPlanDetail = getCodexPlanDetailLabel(quota); return variant.audience !== 'unknown' ? variant.audience : getCodexPlanAudience(quota);
return variant.detailLabel ?? variant.compactDetailLabel ?? codexPlanDetail;
}
function getVariantCompactDetailLabel(
variant: {
audience: string;
compactDetailLabel?: string | null;
detailLabel?: string | null;
},
quota?: unknown
) {
const codexPlanDetail = getCodexPlanDetailLabel(quota);
return variant.compactDetailLabel ?? variant.detailLabel ?? codexPlanDetail;
} }
function getVariantInlineLabel( function getVariantInlineLabel(
variant: { variant: {
audience: string; audience: AccountAudience;
audienceLabel?: string | null; audienceLabel?: string | null;
detailLabel?: string | null; detailLabel?: string | null;
compactDetailLabel?: string | null; compactDetailLabel?: string | null;
@@ -135,14 +146,16 @@ function getVariantInlineLabel(
}, },
quota?: unknown quota?: unknown
) { ) {
const detailLabel = getVariantDetailLabel(variant, quota); const detailLabel = getVariantDetailLabel(variant);
const composedLabel = [variant.audienceLabel, detailLabel].filter(Boolean).join(' · '); const audienceLabel =
variant.audienceLabel ?? getDetailedAudienceLabel(getVariantAudience(variant, quota));
const composedLabel = [audienceLabel, detailLabel].filter(Boolean).join(' · ');
return composedLabel || variant.inlineLabel || null; return composedLabel || variant.inlineLabel || null;
} }
function getVariantMarkerLabel( function getVariantMarkerLabel(
variant: { variant: {
audience: string; audience: AccountAudience;
audienceLabel?: string | null; audienceLabel?: string | null;
detailLabel?: string | null; detailLabel?: string | null;
compactDetailLabel?: string | null; compactDetailLabel?: string | null;
@@ -150,23 +163,30 @@ function getVariantMarkerLabel(
audienceCounts: Map<string, number>, audienceCounts: Map<string, number>,
quota?: unknown quota?: unknown
) { ) {
const compactDetailLabel = getVariantCompactDetailLabel(variant, quota); const audience = getVariantAudience(variant, quota);
if (variant.audience === 'business') { const compactDetailLabel = getVariantCompactDetailLabel(variant);
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';
} }
if (variant.audience === 'personal') { if (audience === 'free') {
return compactDetailLabel ?? 'Free';
}
if (audience === 'personal') {
return compactDetailLabel ?? 'Pers'; return compactDetailLabel ?? 'Pers';
} }
const normalizedFallback = const normalizedFallback =
compactDetailLabel?.trim() || variant.audienceLabel?.trim() || variant.detailLabel?.trim(); compactDetailLabel?.trim() ||
variant.audienceLabel?.trim() ||
getDetailedAudienceLabel(audience) ||
variant.detailLabel?.trim();
return normalizedFallback?.[0]?.toUpperCase() ?? '?'; return normalizedFallback?.[0]?.toUpperCase() ?? '?';
} }
function getGroupedVariantSummaryLabel( function getGroupedVariantSummaryLabel(
variants: Array<{ variants: Array<{
audience: string; audience: AccountAudience;
audienceLabel?: string | null; audienceLabel?: string | null;
detailLabel?: string | null; detailLabel?: string | null;
compactDetailLabel?: string | null; compactDetailLabel?: string | null;
@@ -176,7 +196,10 @@ 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(getVariantCompactDetailLabel(variant, quotas[index])) Boolean(
getVariantCompactDetailLabel(variant) ||
getDetailedAudienceLabel(getVariantAudience(variant, quotas[index]))
)
); );
if ( if (
@@ -274,9 +297,11 @@ export function AccountCard({
index > 0 && 'border-l border-border/50', index > 0 && 'border-l border-border/50',
variant.audience === 'business' variant.audience === 'business'
? 'bg-sky-500/12 text-sky-700 dark:bg-sky-500/20 dark:text-sky-300' ? 'bg-sky-500/12 text-sky-700 dark:bg-sky-500/20 dark:text-sky-300'
: variant.audience === 'personal' : variant.audience === 'free'
? 'bg-emerald-500/12 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300' ? 'bg-slate-200/70 text-slate-700 dark:bg-slate-700/40 dark:text-slate-200'
: 'bg-muted text-muted-foreground' : variant.audience === 'personal'
? 'bg-emerald-500/12 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
: 'bg-muted text-muted-foreground'
)} )}
> >
{getVariantMarkerLabel( {getVariantMarkerLabel(
@@ -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 { formatAccountVariantPart, getAccountIdentityPresentation } from '@/lib/account-identity'; import { 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';
@@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next';
import { AccountQuotaPanel } from './account-quota-panel'; import { AccountQuotaPanel } from './account-quota-panel';
type AccountSurfaceMode = 'compact' | 'detailed'; type AccountSurfaceMode = 'compact' | 'detailed';
type AccountAudience = 'business' | 'free' | 'personal' | 'unknown';
type AccountTier = 'free' | 'pro' | 'ultra' | 'unknown'; type AccountTier = 'free' | 'pro' | 'ultra' | 'unknown';
interface AccountSurfaceCardProps { interface AccountSurfaceCardProps {
@@ -36,11 +37,15 @@ interface AccountSurfaceCardProps {
className?: string; className?: string;
} }
function getAudienceBadgeClass(audience: 'business' | 'personal' | 'unknown') { function getAudienceBadgeClass(audience: AccountAudience) {
if (audience === 'business') { if (audience === 'business') {
return 'bg-sky-500/12 text-sky-700 dark:text-sky-300'; return 'bg-sky-500/12 text-sky-700 dark:text-sky-300';
} }
if (audience === 'free') {
return 'bg-slate-200/70 text-slate-700 dark:bg-slate-700/40 dark:text-slate-200';
}
if (audience === 'personal') { if (audience === 'personal') {
return 'bg-emerald-500/12 text-emerald-700 dark:text-emerald-300'; return 'bg-emerald-500/12 text-emerald-700 dark:text-emerald-300';
} }
@@ -54,20 +59,29 @@ function getTierBadgeClass(tier: AccountTier | undefined) {
: 'bg-yellow-500/15 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-400'; : 'bg-yellow-500/15 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-400';
} }
function getCompactAudienceBadgeLabel( function getCompactAudienceBadgeLabel(audience: AccountAudience, t: (key: string) => string) {
audience: 'business' | 'personal' | 'unknown',
t: (key: string) => string
) {
if (audience === 'business') return t('accountSurfaceCard.business'); if (audience === 'business') return t('accountSurfaceCard.business');
if (audience === 'free') return t('accountSurfaceCard.free');
if (audience === 'personal') return t('accountSurfaceCard.personal'); if (audience === 'personal') return t('accountSurfaceCard.personal');
return '?'; return '?';
} }
function getCompactDetailBadgeClass(audience: 'business' | 'personal' | 'unknown') { function getDetailedAudienceLabel(audience: AccountAudience): string | null {
if (audience === 'business') return 'Business';
if (audience === 'free') return 'Free';
if (audience === 'personal') return 'Personal';
return null;
}
function getCompactDetailBadgeClass(audience: AccountAudience) {
if (audience === 'business') { if (audience === 'business') {
return 'border-sky-500/30 bg-sky-500/10 text-sky-700 dark:border-sky-400/30 dark:bg-sky-500/15 dark:text-sky-200'; return 'border-sky-500/30 bg-sky-500/10 text-sky-700 dark:border-sky-400/30 dark:bg-sky-500/15 dark:text-sky-200';
} }
if (audience === 'free') {
return 'border-slate-300/70 bg-slate-100/80 text-slate-700 dark:border-slate-500/40 dark:bg-slate-700/30 dark:text-slate-200';
}
if (audience === 'personal') { if (audience === 'personal') {
return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:border-emerald-400/30 dark:bg-emerald-500/15 dark:text-emerald-200'; return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:border-emerald-400/30 dark:bg-emerald-500/15 dark:text-emerald-200';
} }
@@ -88,12 +102,15 @@ function resolveEffectiveTier(
return tier; return tier;
} }
function getCodexPlanDetailLabel(quota: UnifiedQuotaResult | undefined): string | null { function getCodexPlanAudience(quota: UnifiedQuotaResult | undefined): AccountAudience {
if (!quota || !('planType' in quota) || !quota.planType) { if (!quota || !('planType' in quota) || !quota.planType) {
return null; return 'unknown';
} }
return formatAccountVariantPart(quota.planType); if (quota.planType === 'team') return 'business';
if (quota.planType === 'free') return 'free';
if (quota.planType === 'plus') return 'personal';
return 'unknown';
} }
export function AccountSurfaceCard({ export function AccountSurfaceCard({
@@ -124,10 +141,13 @@ export function AccountSurfaceCard({
const title = displayEmail || identity.email || accountId; const title = displayEmail || identity.email || accountId;
const normalizedProvider = provider.toLowerCase(); const normalizedProvider = provider.toLowerCase();
const effectiveTier = resolveEffectiveTier(tier, quota); const effectiveTier = resolveEffectiveTier(tier, quota);
const codexPlanDetailLabel = const codexPlanAudience =
normalizedProvider === 'codex' ? getCodexPlanDetailLabel(quota) : null; normalizedProvider === 'codex' ? getCodexPlanAudience(quota) : 'unknown';
const resolvedDetailLabel = identity.detailLabel ?? codexPlanDetailLabel; const effectiveAudience = identity.audience !== 'unknown' ? identity.audience : codexPlanAudience;
const resolvedCompactDetailLabel = identity.compactDetailLabel ?? codexPlanDetailLabel; const effectiveAudienceLabel =
identity.audienceLabel ?? getDetailedAudienceLabel(effectiveAudience);
const resolvedDetailLabel = identity.detailLabel;
const resolvedCompactDetailLabel = identity.compactDetailLabel;
const showTierBadge = const showTierBadge =
(normalizedProvider === 'agy' || (normalizedProvider === 'agy' ||
normalizedProvider === 'antigravity' || normalizedProvider === 'antigravity' ||
@@ -148,17 +168,15 @@ export function AccountSurfaceCard({
{effectiveTier} {effectiveTier}
</span> </span>
)} )}
{identity.audienceLabel && ( {effectiveAudienceLabel && (
<span <span
title={identity.audienceLabel} title={effectiveAudienceLabel}
className={cn( className={cn(
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md shrink-0', 'text-[8px] font-semibold px-1.5 py-0.5 rounded-md shrink-0',
identity.audience === 'business' getAudienceBadgeClass(effectiveAudience)
? 'bg-sky-500/15 text-sky-700 dark:bg-sky-500/25 dark:text-sky-300'
: 'bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/25 dark:text-emerald-300'
)} )}
> >
{getCompactAudienceBadgeLabel(identity.audience, t)} {getCompactAudienceBadgeLabel(effectiveAudience, t)}
</span> </span>
)} )}
{resolvedCompactDetailLabel && ( {resolvedCompactDetailLabel && (
@@ -166,7 +184,7 @@ export function AccountSurfaceCard({
title={resolvedDetailLabel ?? resolvedCompactDetailLabel} title={resolvedDetailLabel ?? resolvedCompactDetailLabel}
className={cn( className={cn(
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md border shrink-0', 'text-[8px] font-semibold px-1.5 py-0.5 rounded-md border shrink-0',
getCompactDetailBadgeClass(identity.audience) getCompactDetailBadgeClass(effectiveAudience)
)} )}
> >
{resolvedCompactDetailLabel} {resolvedCompactDetailLabel}
@@ -227,15 +245,15 @@ export function AccountSurfaceCard({
{title} {title}
</span> </span>
{isCompact && (compactMetaBadges ?? defaultCompactMetaBadges)} {isCompact && (compactMetaBadges ?? defaultCompactMetaBadges)}
{!isCompact && identity.audienceLabel && ( {!isCompact && effectiveAudienceLabel && (
<Badge <Badge
variant="outline" variant="outline"
className={cn( className={cn(
'text-[10px] h-4 px-1.5 border-transparent', 'text-[10px] h-4 px-1.5 border-transparent',
getAudienceBadgeClass(identity.audience) getAudienceBadgeClass(effectiveAudience)
)} )}
> >
{identity.audienceLabel} {effectiveAudienceLabel}
</Badge> </Badge>
)} )}
{!isCompact && resolvedDetailLabel && ( {!isCompact && resolvedDetailLabel && (
@@ -53,7 +53,9 @@ export function AccountStep({
'text-[10px] h-4 px-1.5 border-transparent', 'text-[10px] h-4 px-1.5 border-transparent',
identity.audience === 'business' identity.audience === 'business'
? 'bg-sky-500/12 text-sky-700 dark:text-sky-300' ? 'bg-sky-500/12 text-sky-700 dark:text-sky-300'
: 'bg-emerald-500/12 text-emerald-700 dark:text-emerald-300' : identity.audience === 'free'
? 'bg-slate-200/70 text-slate-700 dark:bg-slate-700/40 dark:text-slate-200'
: 'bg-emerald-500/12 text-emerald-700 dark:text-emerald-300'
)} )}
> >
{identity.audienceLabel} {identity.audienceLabel}
@@ -87,7 +87,9 @@ export function VariantStep({
'text-[10px] h-4 px-1.5 border-transparent', 'text-[10px] h-4 px-1.5 border-transparent',
selectedAccountIdentity.audience === 'business' selectedAccountIdentity.audience === 'business'
? 'bg-sky-500/12 text-sky-700 dark:text-sky-300' ? 'bg-sky-500/12 text-sky-700 dark:text-sky-300'
: 'bg-emerald-500/12 text-emerald-700 dark:text-emerald-300' : selectedAccountIdentity.audience === 'free'
? 'bg-slate-200/70 text-slate-700 dark:bg-slate-700/40 dark:text-slate-200'
: 'bg-emerald-500/12 text-emerald-700 dark:text-emerald-300'
)} )}
> >
{selectedAccountIdentity.audienceLabel} {selectedAccountIdentity.audienceLabel}
+24 -11
View File
@@ -1,10 +1,11 @@
const PERSONAL_PLAN_PARTS = new Set(['free', 'plus', 'pro']); const FREE_PLAN_PARTS = new Set(['free']);
const PERSONAL_PLAN_PARTS = new Set(['plus', 'pro']);
const BUSINESS_PLAN_PARTS = new Set(['team']); const BUSINESS_PLAN_PARTS = new Set(['team']);
// Keep variant parsing aligned with src/cliproxy/accounts/email-account-identity.ts. // Keep variant parsing aligned with src/cliproxy/accounts/email-account-identity.ts.
// This browser copy stays local because the server module is not bundle-safe for the UI. // This browser copy stays local because the server module is not bundle-safe for the UI.
export type AccountAudience = 'business' | 'personal' | 'unknown'; export type AccountAudience = 'business' | 'free' | 'personal' | 'unknown';
export interface AccountIdentityPresentation { export interface AccountIdentityPresentation {
email: string; email: string;
@@ -105,11 +106,16 @@ function formatWorkspaceLabel(parts: string[]): {
const extraLabel = parts.map(formatAccountVariantPart).filter(Boolean).join(' · '); const extraLabel = parts.map(formatAccountVariantPart).filter(Boolean).join(' · ');
return { return {
detailLabel: extraLabel || 'Team', // TODO i18n: missing key for team fallback detailLabel: extraLabel || null,
compactDetailLabel: extraLabel || 'Team', compactDetailLabel: extraLabel || null,
}; };
} }
function formatAudienceDetail(parts: string[]): string | null {
const label = parts.map(formatAccountVariantPart).filter(Boolean).join(' · ');
return label || null;
}
export function extractAccountVariantKey( export function extractAccountVariantKey(
accountId: string, accountId: string,
email?: string, email?: string,
@@ -166,14 +172,21 @@ export function getAccountIdentityPresentation(
}; };
} }
if (suffix && FREE_PLAN_PARTS.has(suffix)) {
const detailLabel = formatAudienceDetail(parts.slice(0, -1));
const inlineLabel = ['Free', detailLabel].filter(Boolean).join(' · '); // TODO i18n: missing key for Free
return {
email: resolvedEmail,
audience: 'free',
audienceLabel: 'Free',
detailLabel,
compactDetailLabel: detailLabel,
inlineLabel,
};
}
if (suffix && PERSONAL_PLAN_PARTS.has(suffix)) { if (suffix && PERSONAL_PLAN_PARTS.has(suffix)) {
const detailParts = [ const detailLabel = formatAudienceDetail(parts.slice(0, -1));
formatAccountVariantPart(suffix),
...parts.slice(0, -1).map(formatAccountVariantPart),
]
.filter(Boolean)
.join(' · ');
const detailLabel = detailParts || formatAccountVariantPart(suffix);
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,
+2 -1
View File
@@ -39,7 +39,8 @@ export interface AccountVisualGroup {
const AUDIENCE_ORDER: Record<AccountAudience, number> = { const AUDIENCE_ORDER: Record<AccountAudience, number> = {
business: 0, business: 0,
personal: 1, personal: 1,
unknown: 2, free: 2,
unknown: 3,
}; };
function getLatestTimestamp(current?: string, candidate?: string): string | undefined { function getLatestTimestamp(current?: string, candidate?: string): string | undefined {
+4
View File
@@ -1701,6 +1701,7 @@ const resources = {
}, },
accountSurfaceCard: { accountSurfaceCard: {
business: 'Biz', business: 'Biz',
free: 'Free',
personal: 'Pers', personal: 'Pers',
variant: 'Variant', variant: 'Variant',
}, },
@@ -4049,6 +4050,7 @@ const resources = {
}, },
accountSurfaceCard: { accountSurfaceCard: {
business: '企业', business: '企业',
free: '免费',
personal: '个人', personal: '个人',
variant: '变体', variant: '变体',
}, },
@@ -6469,6 +6471,7 @@ const resources = {
}, },
accountSurfaceCard: { accountSurfaceCard: {
business: 'Biz', business: 'Biz',
free: 'Miễn phí',
personal: 'Cá nhân', personal: 'Cá nhân',
variant: 'Biến thể', variant: 'Biến thể',
}, },
@@ -8671,6 +8674,7 @@ const resources = {
}, },
accountSurfaceCard: { accountSurfaceCard: {
business: 'ビジネス', business: 'ビジネス',
free: '無料',
personal: '個人', personal: '個人',
variant: 'バリアント', variant: 'バリアント',
}, },
@@ -87,11 +87,11 @@ const groupedAccount: AccountData = {
isDefault: true, isDefault: true,
successCount: 4, successCount: 4,
failureCount: 1, failureCount: 1,
audience: 'personal', audience: 'free',
audienceLabel: 'Personal', audienceLabel: 'Free',
detailLabel: 'Free', detailLabel: null,
compactDetailLabel: 'Free', compactDetailLabel: null,
inlineLabel: 'Personal · Free', inlineLabel: 'Free',
}, },
], ],
}; };
@@ -99,9 +99,11 @@ const groupedAccount: AccountData = {
const groupedAccountWithProPersonal: AccountData = { const groupedAccountWithProPersonal: AccountData = {
...groupedAccount, ...groupedAccount,
variants: groupedAccount.variants?.map((variant) => variants: groupedAccount.variants?.map((variant) =>
variant.audience === 'personal' variant.audience === 'free'
? { ? {
...variant, ...variant,
audience: 'personal',
audienceLabel: 'Personal',
detailLabel: 'Pro', detailLabel: 'Pro',
compactDetailLabel: 'Pro', compactDetailLabel: 'Pro',
inlineLabel: 'Personal · Pro', inlineLabel: 'Personal · Pro',
@@ -148,9 +150,7 @@ describe('AccountCard grouped quota tooltip', () => {
/> />
); );
expect( expect(screen.getByTitle('Business · Workspace 04a0f049 • Free')).toBeInTheDocument();
screen.getByTitle('Business · Workspace 04a0f049 • Personal · Free')
).toBeInTheDocument();
expect(screen.getByText('Biz')).toBeInTheDocument(); expect(screen.getByText('Biz')).toBeInTheDocument();
await userEvent.hover(screen.getByText('Business · Workspace 04a0f049')); await userEvent.hover(screen.getByText('Business · Workspace 04a0f049'));
@@ -164,7 +164,14 @@ describe('AccountCard grouped quota tooltip', () => {
expect(tooltipContent?.className).toContain('text-popover-foreground'); expect(tooltipContent?.className).toContain('text-popover-foreground');
expect(tooltipContent?.className).toContain('max-w-[calc(100vw-2rem)]'); expect(tooltipContent?.className).toContain('max-w-[calc(100vw-2rem)]');
await userEvent.hover(screen.getByText('Personal · Free')); const freeLabels = screen.getAllByText('Free');
const quotaLabel = freeLabels[freeLabels.length - 1];
expect(quotaLabel).toBeDefined();
if (!quotaLabel) {
throw new Error('Expected a Free quota label');
}
await userEvent.hover(quotaLabel);
const personalPlan = (await screen.findAllByText('Plan: free')).find((node) => const personalPlan = (await screen.findAllByText('Plan: free')).find((node) =>
node.closest('[data-slot="tooltip-content"]') node.closest('[data-slot="tooltip-content"]')
); );
@@ -193,6 +200,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('Personal · Free')).not.toBeInTheDocument(); expect(screen.queryByText('Free')).not.toBeInTheDocument();
}); });
}); });
@@ -57,7 +57,7 @@ describe('AccountSurfaceCard', () => {
expect(screen.getByText('pro')).toBeInTheDocument(); expect(screen.getByText('pro')).toBeInTheDocument();
}); });
it('shows both personal identity and free-tier detail for compact Codex cards', () => { it('shows free Codex accounts as a single standalone audience badge', () => {
render( render(
<AccountSurfaceCard <AccountSurfaceCard
mode="compact" mode="compact"
@@ -70,12 +70,12 @@ describe('AccountSurfaceCard', () => {
/> />
); );
expect(screen.getByText('Pers')).toBeInTheDocument();
expect(screen.getByTitle('Personal')).toBeInTheDocument();
expect(screen.getByText('Free')).toBeInTheDocument(); expect(screen.getByText('Free')).toBeInTheDocument();
expect(screen.getByTitle('Free')).toBeInTheDocument();
expect(screen.queryByText('Pers')).not.toBeInTheDocument();
}); });
it('keeps richer token-derived Codex personal detail when live quota planType is coarser', () => { it('keeps the simplified personal audience when live quota planType is coarser', () => {
render( render(
<AccountSurfaceCard <AccountSurfaceCard
mode="compact" mode="compact"
@@ -89,7 +89,24 @@ describe('AccountSurfaceCard', () => {
/> />
); );
expect(screen.getByText('Pro')).toBeInTheDocument(); expect(screen.getByText('Pers')).toBeInTheDocument();
expect(screen.queryByText('Free')).not.toBeInTheDocument(); expect(screen.queryByText('Free')).not.toBeInTheDocument();
}); });
it('falls back to a single free badge when Codex quota detects a free plan', () => {
render(
<AccountSurfaceCard
mode="compact"
provider="codex"
accountId="user@example.com"
email="user@example.com"
displayEmail="user@example.com"
quota={createCodexQuotaResult({ planType: 'free' })}
showQuota={false}
/>
);
expect(screen.getByText('Free')).toBeInTheDocument();
expect(screen.queryByText('Pers')).not.toBeInTheDocument();
});
}); });
+21 -2
View File
@@ -39,14 +39,33 @@ describe('account identity presentation', () => {
).toBe('Business · Workspace 04a0f049'); ).toBe('Business · Workspace 04a0f049');
}); });
it('formats personal codex plans deliberately instead of leaking raw free suffixes', () => { it('classifies free codex accounts as a standalone audience', () => {
const presentation = getAccountIdentityPresentation(
'kaidu.kd@gmail.com',
'kaidu.kd@gmail.com',
'codex-kaidu.kd@gmail.com-free.json'
);
expect(presentation.audience).toBe('free');
expect(presentation.audienceLabel).toBe('Free');
expect(presentation.detailLabel).toBeNull();
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-free.json' 'codex-kaidu.kd@gmail.com-free.json'
) )
).toBe('kaidu.kd@gmail.com (Personal · Free)'); ).toBe('kaidu.kd@gmail.com (Free)');
});
it('collapses paid codex personal plans into a single personal audience label', () => {
expect(
formatAccountDisplayName(
'kaidu.kd@gmail.com',
'kaidu.kd@gmail.com',
'codex-kaidu.kd@gmail.com-plus.json'
)
).toBe('kaidu.kd@gmail.com (Personal)');
}); });
it('leaves plain accounts without inferred state untouched', () => { it('leaves plain accounts without inferred state untouched', () => {