diff --git a/ui/src/lib/account-visual-groups.ts b/ui/src/lib/account-visual-groups.ts index 7e578d92..7d9a1ec7 100644 --- a/ui/src/lib/account-visual-groups.ts +++ b/ui/src/lib/account-visual-groups.ts @@ -34,6 +34,12 @@ export interface AccountVisualGroup { variants?: AccountVisualVariant[]; } +const AUDIENCE_ORDER: Record = { + business: 0, + personal: 1, + unknown: 2, +}; + function getLatestTimestamp(current?: string, candidate?: string): string | undefined { if (!candidate) return current; if (!current) return candidate; @@ -64,6 +70,20 @@ function buildAccountVariant( }; } +function sortAccountVariants(variants: AccountVisualVariant[]): AccountVisualVariant[] { + return [...variants].sort((left, right) => { + const audienceDelta = AUDIENCE_ORDER[left.audience] - AUDIENCE_ORDER[right.audience]; + if (audienceDelta !== 0) { + return audienceDelta; + } + + const leftLabel = left.audienceLabel ?? left.detailLabel ?? left.id; + const rightLabel = right.audienceLabel ?? right.detailLabel ?? right.id; + + return leftLabel.localeCompare(rightLabel); + }); +} + export function buildAccountVisualGroups( accounts: OAuthAccount[], statsData?: Pick | null @@ -104,24 +124,25 @@ export function buildAccountVisualGroups( }; } - const canonicalEmail = variants[0]?.email ?? bucketKey; - const originalProvider = accountMeta.get(variants[0]?.id ?? '')?.provider ?? 'codex'; + const orderedVariants = sortAccountVariants(variants); + const canonicalEmail = orderedVariants[0]?.email ?? bucketKey; + const originalProvider = accountMeta.get(orderedVariants[0]?.id ?? '')?.provider ?? 'codex'; return { id: bucketKey, email: canonicalEmail, - tokenFile: variants[0]?.tokenFile ?? '', + tokenFile: orderedVariants[0]?.tokenFile ?? '', provider: originalProvider, - isDefault: variants.some((variant) => variant.isDefault), - successCount: variants.reduce((sum, variant) => sum + variant.successCount, 0), - failureCount: variants.reduce((sum, variant) => sum + variant.failureCount, 0), - lastUsedAt: variants.reduce( + isDefault: orderedVariants.some((variant) => variant.isDefault), + successCount: orderedVariants.reduce((sum, variant) => sum + variant.successCount, 0), + failureCount: orderedVariants.reduce((sum, variant) => sum + variant.failureCount, 0), + lastUsedAt: orderedVariants.reduce( (latest, variant) => getLatestTimestamp(latest, variant.lastUsedAt), undefined ), - paused: variants.every((variant) => Boolean(variant.paused)), - memberIds: variants.map((variant) => variant.id), - variants, + paused: orderedVariants.every((variant) => Boolean(variant.paused)), + memberIds: orderedVariants.map((variant) => variant.id), + variants: orderedVariants, }; }); } diff --git a/ui/tests/unit/ui/lib/account-visual-groups.test.ts b/ui/tests/unit/ui/lib/account-visual-groups.test.ts new file mode 100644 index 00000000..2c654071 --- /dev/null +++ b/ui/tests/unit/ui/lib/account-visual-groups.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; + +import type { OAuthAccount } from '@/lib/api-client'; +import { buildAccountVisualGroups } from '@/lib/account-visual-groups'; + +function makeAccount(overrides: Partial & Pick) { + return { + id: overrides.id, + email: 'kaidu.kd@gmail.com', + provider: 'codex', + isDefault: false, + tokenFile: overrides.tokenFile, + createdAt: '2026-03-30T00:00:00.000Z', + ...overrides, + } satisfies OAuthAccount; +} + +describe('buildAccountVisualGroups', () => { + it('orders grouped codex variants by audience consistently', () => { + const groups = buildAccountVisualGroups([ + makeAccount({ + id: 'kaidu.kd@gmail.com#free', + tokenFile: 'codex-kaidu.kd@gmail.com-free.json', + }), + makeAccount({ + id: 'kaidu.kd@gmail.com#04a0f049-team', + tokenFile: 'codex-04a0f049-kaidu.kd@gmail.com-team.json', + }), + ]); + + expect(groups).toHaveLength(1); + expect(groups[0]?.variants?.map((variant) => variant.audience)).toEqual([ + 'business', + 'personal', + ]); + expect(groups[0]?.memberIds).toEqual([ + 'kaidu.kd@gmail.com#04a0f049-team', + 'kaidu.kd@gmail.com#free', + ]); + }); +});