fix(ui): deduplicate usage profiles and make dropdowns scrollable

- Deduplicate ProfileUsageSummary entries by email in usage-monitor.ts
  so multiple ClaudeProfileManager profiles sharing the same underlying
  OAuth account (different configDir) collapse to one entry
- Add max-h-[80vh] overflow-y-auto to all UsageIndicator PopoverContent
  instances so the dropdown scrolls with many accounts
- Add max-h-[60vh] overflow-y-auto to GitHubSetupModal "Connect to AI"
  provider list so Continue/Skip buttons stay visible
- Restructure onboarding AccountsStep with pinned header/footer and
  independently scrollable provider list

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
AndyMik90
2026-03-14 09:25:35 +01:00
co-authored by Claude Opus 4.6
parent 53b55468c9
commit 33ed7d33fe
4 changed files with 60 additions and 21 deletions
@@ -442,6 +442,9 @@ export class UsageMonitor extends EventEmitter {
// Include Z.AI provider accounts from providerAccounts
await this.appendZAIAccounts(allProfiles);
// Deduplicate by email (multiple profiles can share the same underlying account)
const deduped = this.deduplicateProfilesByEmail(allProfiles);
// Return minimal data with auth status - don't return null!
return {
activeProfile: {
@@ -452,7 +455,7 @@ export class UsageMonitor extends EventEmitter {
fetchedAt: new Date(),
needsReauthentication: this.needsReauthProfiles.has(activeProfileId || '')
},
allProfiles,
allProfiles: deduped,
fetchedAt: new Date()
};
}
@@ -752,17 +755,49 @@ export class UsageMonitor extends EventEmitter {
// Include Z.AI provider accounts from providerAccounts
await this.appendZAIAccounts(allProfiles);
// Deduplicate by email (multiple profiles can share the same underlying account)
const deduplicatedProfiles = this.deduplicateProfilesByEmail(allProfiles);
// Sort by availability score (highest first = most available)
allProfiles.sort((a, b) => b.availabilityScore - a.availabilityScore);
deduplicatedProfiles.sort((a, b) => b.availabilityScore - a.availabilityScore);
return {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
activeProfile: this.currentUsage!, // Non-null: _doGetAllProfilesUsage is only called when currentUsage is set
allProfiles,
allProfiles: deduplicatedProfiles,
fetchedAt: new Date()
};
}
/**
* Deduplicate profiles by email. Multiple ClaudeProfileManager profiles can share
* the same email (different configDir pointing to the same underlying OAuth account).
* Keeps one entry per email, preferring the one with higher usage or active status.
*/
private deduplicateProfilesByEmail(profiles: ProfileUsageSummary[]): ProfileUsageSummary[] {
const result: ProfileUsageSummary[] = [];
const seenEmails = new Map<string, number>(); // email -> index in result
for (const profile of profiles) {
const key = profile.profileEmail?.toLowerCase();
if (key && seenEmails.has(key)) {
const existingIdx = seenEmails.get(key)!;
const existing = result[existingIdx];
const existingMax = Math.max(existing.sessionPercent, existing.weeklyPercent);
const currentMax = Math.max(profile.sessionPercent, profile.weeklyPercent);
if (currentMax > existingMax || (currentMax === existingMax && profile.isActive)) {
result[existingIdx] = profile;
}
} else {
seenEmails.set(key ?? profile.profileId, result.length);
result.push(profile);
}
}
if (result.length < profiles.length) {
console.log('[UsageMonitor] Deduplicated profiles by email:', profiles.length, '→', result.length);
}
return result;
}
/**
* Fetch usage for an inactive profile using its own credentials
* This allows showing real usage data for non-active profiles
@@ -400,11 +400,11 @@ export function GitHubSetupModal({
</DialogDescription>
</DialogHeader>
<div className="py-4 space-y-4">
<div className="py-4 space-y-4 max-h-[60vh] overflow-y-auto">
<ProviderAccountsList />
{getProviderAccounts().length > 0 && (
<div className="flex items-center gap-2 rounded-lg bg-success/10 border border-success/30 p-3">
<div className="flex items-center gap-2 rounded-lg bg-success/10 border border-success/30 p-3 sticky bottom-0">
<CheckCircle2 className="h-4 w-4 text-success shrink-0" />
<p className="text-sm text-success">
{t('githubSetup.aiProviderReady')}
@@ -828,7 +828,7 @@ export function UsageIndicator() {
<PopoverContent
side="bottom"
align="end"
className="text-xs w-72 p-0"
className="text-xs w-72 p-0 max-h-[80vh] overflow-y-auto"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
@@ -959,7 +959,7 @@ export function UsageIndicator() {
<PopoverContent
side="bottom"
align="end"
className="text-xs w-72 p-0"
className="text-xs w-72 p-0 max-h-[80vh] overflow-y-auto"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
@@ -1099,7 +1099,7 @@ export function UsageIndicator() {
<PopoverContent
side="bottom"
align="end"
className="text-xs w-72 p-0"
className="text-xs w-72 p-0 max-h-[80vh] overflow-y-auto"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
@@ -1317,7 +1317,7 @@ export function UsageIndicator() {
<PopoverContent
side="bottom"
align="end"
className="text-xs w-72 p-0"
className="text-xs w-72 p-0 max-h-[80vh] overflow-y-auto"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
@@ -15,35 +15,39 @@ interface AccountsStepProps {
* Replaces the old AuthChoiceStep + OAuthStep two-step flow with a single
* step that reuses the ProviderAccountsList from settings. Users can add
* accounts from any supported provider (Anthropic, OpenAI, Google, etc.).
*
* Layout: The header and action buttons are pinned (always visible), while
* the provider list scrolls independently. This prevents the "Continue"
* button from being hidden below the fold on smaller screens.
*/
export function AccountsStep({ onNext, onBack, onSkip }: AccountsStepProps) {
const { t } = useTranslation('onboarding');
return (
<div className="flex h-full flex-col items-center px-8 py-6">
<div className="w-full max-w-2xl">
{/* Header */}
<div className="text-center mb-8">
<div className="flex justify-center mb-4">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
<Users className="h-8 w-8 text-primary" />
<div className="w-full max-w-2xl flex flex-col min-h-0 h-full">
{/* Header — pinned at top */}
<div className="text-center mb-6 flex-shrink-0">
<div className="flex justify-center mb-3">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<Users className="h-6 w-6 text-primary" />
</div>
</div>
<h1 className="text-3xl font-bold text-foreground tracking-tight">
<h1 className="text-2xl font-bold text-foreground tracking-tight">
{t('accounts.title')}
</h1>
<p className="mt-3 text-muted-foreground text-lg">
<p className="mt-2 text-muted-foreground text-base">
{t('accounts.description')}
</p>
</div>
{/* Provider accounts list - reused from settings */}
<div className="rounded-lg border border-border bg-card/50 p-4">
{/* Provider accounts list — scrollable */}
<div className="flex-1 min-h-0 overflow-y-auto rounded-lg border border-border bg-card/50 p-4">
<ProviderAccountsList />
</div>
{/* Action Buttons */}
<div className="flex justify-between items-center mt-10 pt-6 border-t border-border">
{/* Action Buttons — pinned at bottom, always visible */}
<div className="flex justify-between items-center mt-4 pt-4 border-t border-border flex-shrink-0">
<Button
variant="ghost"
onClick={onBack}