diff --git a/src/cliproxy/accounts/email-account-identity.ts b/src/cliproxy/accounts/email-account-identity.ts index 35c36bd1..7d6c73a1 100644 --- a/src/cliproxy/accounts/email-account-identity.ts +++ b/src/cliproxy/accounts/email-account-identity.ts @@ -1,6 +1,9 @@ import type { CLIProxyProvider } from '../types'; const DUPLICATE_EMAIL_ACCOUNT_PROVIDERS = new Set(['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 // 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 { 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(); - if (suffix && ['team', 'free', 'plus', 'pro'].includes(suffix)) { - return [formatVariantPart(suffix), ...parts.slice(0, -1).map(formatVariantPart)] - .filter(Boolean) - .join(' · '); + if (suffix && BUSINESS_PLAN_PARTS.has(suffix)) { + return ['Business', formatWorkspaceLabel(parts.slice(0, -1))].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(' · '); diff --git a/ui/src/components/account/flow-viz/account-card.tsx b/ui/src/components/account/flow-viz/account-card.tsx index e1ecf92f..7b6f399f 100644 --- a/ui/src/components/account/flow-viz/account-card.tsx +++ b/ui/src/components/account/flow-viz/account-card.tsx @@ -13,7 +13,6 @@ import { getProviderResetTime, getQuotaFailureInfo, } from '@/lib/utils'; -import { formatAccountVariantPart } from '@/lib/account-identity'; import { GripVertical, Loader2, Pause, Play } from 'lucide-react'; import { useAccountQuota, @@ -28,6 +27,7 @@ import { AccountCardStats } from './account-card-stats'; import { cleanEmail } from './utils'; type Zone = 'left' | 'right' | 'top' | 'bottom'; +type AccountAudience = 'business' | 'free' | 'personal' | 'unknown'; const QUOTA_PROVIDER_ALIASES = [ 'antigravity', @@ -88,46 +88,57 @@ function getCompactQuotaColor(percentage: number) { return 'bg-red-500'; } -function getCodexPlanDetailLabel(quota: unknown): string | null { +function getCodexPlanAudience(quota: unknown): AccountAudience { if (!quota || typeof quota !== 'object' || !('planType' in quota)) { - return null; + return 'unknown'; } const planType = (quota as { planType?: unknown }).planType; 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: { - audience: string; - detailLabel?: string | null; - compactDetailLabel?: string | null; + audience: AccountAudience; }, quota?: unknown ) { - const codexPlanDetail = getCodexPlanDetailLabel(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; + return variant.audience !== 'unknown' ? variant.audience : getCodexPlanAudience(quota); } function getVariantInlineLabel( variant: { - audience: string; + audience: AccountAudience; audienceLabel?: string | null; detailLabel?: string | null; compactDetailLabel?: string | null; @@ -135,14 +146,16 @@ function getVariantInlineLabel( }, quota?: unknown ) { - const detailLabel = getVariantDetailLabel(variant, quota); - const composedLabel = [variant.audienceLabel, detailLabel].filter(Boolean).join(' · '); + const detailLabel = getVariantDetailLabel(variant); + const audienceLabel = + variant.audienceLabel ?? getDetailedAudienceLabel(getVariantAudience(variant, quota)); + const composedLabel = [audienceLabel, detailLabel].filter(Boolean).join(' · '); return composedLabel || variant.inlineLabel || null; } function getVariantMarkerLabel( variant: { - audience: string; + audience: AccountAudience; audienceLabel?: string | null; detailLabel?: string | null; compactDetailLabel?: string | null; @@ -150,23 +163,30 @@ function getVariantMarkerLabel( audienceCounts: Map, quota?: unknown ) { - const compactDetailLabel = getVariantCompactDetailLabel(variant, quota); - if (variant.audience === 'business') { + const audience = getVariantAudience(variant, quota); + const compactDetailLabel = getVariantCompactDetailLabel(variant); + if (audience === 'business') { const businessVariantCount = audienceCounts.get('business') ?? 0; return businessVariantCount > 1 && compactDetailLabel ? compactDetailLabel : 'Biz'; } - if (variant.audience === 'personal') { + if (audience === 'free') { + return compactDetailLabel ?? 'Free'; + } + if (audience === 'personal') { return compactDetailLabel ?? 'Pers'; } const normalizedFallback = - compactDetailLabel?.trim() || variant.audienceLabel?.trim() || variant.detailLabel?.trim(); + compactDetailLabel?.trim() || + variant.audienceLabel?.trim() || + getDetailedAudienceLabel(audience) || + variant.detailLabel?.trim(); return normalizedFallback?.[0]?.toUpperCase() ?? '?'; } function getGroupedVariantSummaryLabel( variants: Array<{ - audience: string; + audience: AccountAudience; audienceLabel?: string | null; detailLabel?: string | null; compactDetailLabel?: string | null; @@ -176,7 +196,10 @@ function getGroupedVariantSummaryLabel( ) { const audiences = new Set(variants.map((variant) => variant.audience)); const hasDistinctDetails = variants.some((variant, index) => - Boolean(getVariantCompactDetailLabel(variant, quotas[index])) + Boolean( + getVariantCompactDetailLabel(variant) || + getDetailedAudienceLabel(getVariantAudience(variant, quotas[index])) + ) ); if ( @@ -274,9 +297,11 @@ export function AccountCard({ index > 0 && 'border-l border-border/50', variant.audience === 'business' ? 'bg-sky-500/12 text-sky-700 dark:bg-sky-500/20 dark:text-sky-300' - : variant.audience === 'personal' - ? 'bg-emerald-500/12 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300' - : 'bg-muted text-muted-foreground' + : variant.audience === 'free' + ? 'bg-slate-200/70 text-slate-700 dark:bg-slate-700/40 dark:text-slate-200' + : 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( diff --git a/ui/src/components/account/shared/account-surface-card.tsx b/ui/src/components/account/shared/account-surface-card.tsx index aec49882..21657220 100644 --- a/ui/src/components/account/shared/account-surface-card.tsx +++ b/ui/src/components/account/shared/account-surface-card.tsx @@ -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 { formatAccountVariantPart, getAccountIdentityPresentation } from '@/lib/account-identity'; +import { getAccountIdentityPresentation } from '@/lib/account-identity'; import { cn } from '@/lib/utils'; import { Pause, Star, User } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next'; import { AccountQuotaPanel } from './account-quota-panel'; type AccountSurfaceMode = 'compact' | 'detailed'; +type AccountAudience = 'business' | 'free' | 'personal' | 'unknown'; type AccountTier = 'free' | 'pro' | 'ultra' | 'unknown'; interface AccountSurfaceCardProps { @@ -36,11 +37,15 @@ interface AccountSurfaceCardProps { className?: string; } -function getAudienceBadgeClass(audience: 'business' | 'personal' | 'unknown') { +function getAudienceBadgeClass(audience: AccountAudience) { if (audience === 'business') { 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') { 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'; } -function getCompactAudienceBadgeLabel( - audience: 'business' | 'personal' | 'unknown', - t: (key: string) => string -) { +function getCompactAudienceBadgeLabel(audience: AccountAudience, t: (key: string) => string) { if (audience === 'business') return t('accountSurfaceCard.business'); + if (audience === 'free') return t('accountSurfaceCard.free'); if (audience === 'personal') return t('accountSurfaceCard.personal'); 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') { 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') { 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; } -function getCodexPlanDetailLabel(quota: UnifiedQuotaResult | undefined): string | null { +function getCodexPlanAudience(quota: UnifiedQuotaResult | undefined): AccountAudience { 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({ @@ -124,10 +141,13 @@ export function AccountSurfaceCard({ const title = displayEmail || identity.email || accountId; const normalizedProvider = provider.toLowerCase(); const effectiveTier = resolveEffectiveTier(tier, quota); - const codexPlanDetailLabel = - normalizedProvider === 'codex' ? getCodexPlanDetailLabel(quota) : null; - const resolvedDetailLabel = identity.detailLabel ?? codexPlanDetailLabel; - const resolvedCompactDetailLabel = identity.compactDetailLabel ?? codexPlanDetailLabel; + const codexPlanAudience = + normalizedProvider === 'codex' ? getCodexPlanAudience(quota) : 'unknown'; + const effectiveAudience = identity.audience !== 'unknown' ? identity.audience : codexPlanAudience; + const effectiveAudienceLabel = + identity.audienceLabel ?? getDetailedAudienceLabel(effectiveAudience); + const resolvedDetailLabel = identity.detailLabel; + const resolvedCompactDetailLabel = identity.compactDetailLabel; const showTierBadge = (normalizedProvider === 'agy' || normalizedProvider === 'antigravity' || @@ -148,17 +168,15 @@ export function AccountSurfaceCard({ {effectiveTier} )} - {identity.audienceLabel && ( + {effectiveAudienceLabel && ( - {getCompactAudienceBadgeLabel(identity.audience, t)} + {getCompactAudienceBadgeLabel(effectiveAudience, t)} )} {resolvedCompactDetailLabel && ( @@ -166,7 +184,7 @@ export function AccountSurfaceCard({ title={resolvedDetailLabel ?? resolvedCompactDetailLabel} className={cn( 'text-[8px] font-semibold px-1.5 py-0.5 rounded-md border shrink-0', - getCompactDetailBadgeClass(identity.audience) + getCompactDetailBadgeClass(effectiveAudience) )} > {resolvedCompactDetailLabel} @@ -227,15 +245,15 @@ export function AccountSurfaceCard({ {title} {isCompact && (compactMetaBadges ?? defaultCompactMetaBadges)} - {!isCompact && identity.audienceLabel && ( + {!isCompact && effectiveAudienceLabel && ( - {identity.audienceLabel} + {effectiveAudienceLabel} )} {!isCompact && resolvedDetailLabel && ( diff --git a/ui/src/components/setup/wizard/steps/account-step.tsx b/ui/src/components/setup/wizard/steps/account-step.tsx index 42577953..7e89b232 100644 --- a/ui/src/components/setup/wizard/steps/account-step.tsx +++ b/ui/src/components/setup/wizard/steps/account-step.tsx @@ -53,7 +53,9 @@ export function AccountStep({ 'text-[10px] h-4 px-1.5 border-transparent', identity.audience === 'business' ? '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} diff --git a/ui/src/components/setup/wizard/steps/variant-step.tsx b/ui/src/components/setup/wizard/steps/variant-step.tsx index 311fe496..fb0d3ef1 100644 --- a/ui/src/components/setup/wizard/steps/variant-step.tsx +++ b/ui/src/components/setup/wizard/steps/variant-step.tsx @@ -87,7 +87,9 @@ export function VariantStep({ 'text-[10px] h-4 px-1.5 border-transparent', selectedAccountIdentity.audience === 'business' ? '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} diff --git a/ui/src/lib/account-identity.ts b/ui/src/lib/account-identity.ts index 7b1236a2..7227f3a2 100644 --- a/ui/src/lib/account-identity.ts +++ b/ui/src/lib/account-identity.ts @@ -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']); // 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. -export type AccountAudience = 'business' | 'personal' | 'unknown'; +export type AccountAudience = 'business' | 'free' | 'personal' | 'unknown'; export interface AccountIdentityPresentation { email: string; @@ -105,11 +106,16 @@ function formatWorkspaceLabel(parts: string[]): { const extraLabel = parts.map(formatAccountVariantPart).filter(Boolean).join(' · '); return { - detailLabel: extraLabel || 'Team', // TODO i18n: missing key for team fallback - compactDetailLabel: extraLabel || 'Team', + detailLabel: extraLabel || null, + compactDetailLabel: extraLabel || null, }; } +function formatAudienceDetail(parts: string[]): string | null { + const label = parts.map(formatAccountVariantPart).filter(Boolean).join(' · '); + return label || null; +} + export function extractAccountVariantKey( accountId: 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)) { - const detailParts = [ - formatAccountVariantPart(suffix), - ...parts.slice(0, -1).map(formatAccountVariantPart), - ] - .filter(Boolean) - .join(' · '); - const detailLabel = detailParts || formatAccountVariantPart(suffix); + const detailLabel = formatAudienceDetail(parts.slice(0, -1)); const inlineLabel = ['Personal', detailLabel].filter(Boolean).join(' · '); // TODO i18n: missing key for Personal return { email: resolvedEmail, diff --git a/ui/src/lib/account-visual-groups.ts b/ui/src/lib/account-visual-groups.ts index 259b0634..16a27fde 100644 --- a/ui/src/lib/account-visual-groups.ts +++ b/ui/src/lib/account-visual-groups.ts @@ -39,7 +39,8 @@ export interface AccountVisualGroup { const AUDIENCE_ORDER: Record = { business: 0, personal: 1, - unknown: 2, + free: 2, + unknown: 3, }; function getLatestTimestamp(current?: string, candidate?: string): string | undefined { diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index ce95fc3e..a9cf92bd 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -1701,6 +1701,7 @@ const resources = { }, accountSurfaceCard: { business: 'Biz', + free: 'Free', personal: 'Pers', variant: 'Variant', }, @@ -4049,6 +4050,7 @@ const resources = { }, accountSurfaceCard: { business: '企业', + free: '免费', personal: '个人', variant: '变体', }, @@ -6469,6 +6471,7 @@ const resources = { }, accountSurfaceCard: { business: 'Biz', + free: 'Miễn phí', personal: 'Cá nhân', variant: 'Biến thể', }, @@ -8671,6 +8674,7 @@ const resources = { }, accountSurfaceCard: { business: 'ビジネス', + free: '無料', personal: '個人', variant: 'バリアント', }, diff --git a/ui/tests/unit/components/account/flow-viz/account-card.test.tsx b/ui/tests/unit/components/account/flow-viz/account-card.test.tsx index be9c28d4..5d746854 100644 --- a/ui/tests/unit/components/account/flow-viz/account-card.test.tsx +++ b/ui/tests/unit/components/account/flow-viz/account-card.test.tsx @@ -87,11 +87,11 @@ const groupedAccount: AccountData = { isDefault: true, successCount: 4, failureCount: 1, - audience: 'personal', - audienceLabel: 'Personal', - detailLabel: 'Free', - compactDetailLabel: 'Free', - inlineLabel: 'Personal · Free', + audience: 'free', + audienceLabel: 'Free', + detailLabel: null, + compactDetailLabel: null, + inlineLabel: 'Free', }, ], }; @@ -99,9 +99,11 @@ const groupedAccount: AccountData = { const groupedAccountWithProPersonal: AccountData = { ...groupedAccount, variants: groupedAccount.variants?.map((variant) => - variant.audience === 'personal' + variant.audience === 'free' ? { ...variant, + audience: 'personal', + audienceLabel: 'Personal', detailLabel: 'Pro', compactDetailLabel: 'Pro', inlineLabel: 'Personal · Pro', @@ -148,9 +150,7 @@ describe('AccountCard grouped quota tooltip', () => { /> ); - expect( - screen.getByTitle('Business · Workspace 04a0f049 • Personal · Free') - ).toBeInTheDocument(); + expect(screen.getByTitle('Business · Workspace 04a0f049 • Free')).toBeInTheDocument(); expect(screen.getByText('Biz')).toBeInTheDocument(); 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('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) => 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.getByText('Personal · Pro')).toBeInTheDocument(); - expect(screen.queryByText('Personal · Free')).not.toBeInTheDocument(); + expect(screen.queryByText('Free')).not.toBeInTheDocument(); }); }); diff --git a/ui/tests/unit/ui/components/account/shared/account-surface-card.test.tsx b/ui/tests/unit/ui/components/account/shared/account-surface-card.test.tsx index 56079208..3d0bcb68 100644 --- a/ui/tests/unit/ui/components/account/shared/account-surface-card.test.tsx +++ b/ui/tests/unit/ui/components/account/shared/account-surface-card.test.tsx @@ -57,7 +57,7 @@ describe('AccountSurfaceCard', () => { 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( { /> ); - expect(screen.getByText('Pers')).toBeInTheDocument(); - expect(screen.getByTitle('Personal')).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( { /> ); - expect(screen.getByText('Pro')).toBeInTheDocument(); + expect(screen.getByText('Pers')).toBeInTheDocument(); expect(screen.queryByText('Free')).not.toBeInTheDocument(); }); + + it('falls back to a single free badge when Codex quota detects a free plan', () => { + render( + + ); + + expect(screen.getByText('Free')).toBeInTheDocument(); + expect(screen.queryByText('Pers')).not.toBeInTheDocument(); + }); }); diff --git a/ui/tests/unit/ui/lib/account-identity.test.ts b/ui/tests/unit/ui/lib/account-identity.test.ts index 9ebc93fc..cce62502 100644 --- a/ui/tests/unit/ui/lib/account-identity.test.ts +++ b/ui/tests/unit/ui/lib/account-identity.test.ts @@ -39,14 +39,33 @@ describe('account identity presentation', () => { ).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( formatAccountDisplayName( 'kaidu.kd@gmail.com', 'kaidu.kd@gmail.com', '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', () => {