diff --git a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx index 713b9ffd160f..b4e0a99d762e 100644 --- a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx +++ b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx @@ -21,7 +21,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { SymbolView } from "../../components/AppSymbol"; import { AppText as Text } from "../../components/AppText"; -import { ProviderIcon } from "../../components/ProviderIcon"; +import { ProviderIcon, ProviderInstanceIcon } from "../../components/ProviderIcon"; import { SettingsScreen } from "../settings/components/SettingsScreen"; import { environmentPresentations } from "../../state/presentation"; import { ResetCredits } from "./UsageLimitsSection"; @@ -37,6 +37,28 @@ function accountName(account: LimitAccount) { return `${local[0] ?? ""}${domain[0] ?? ""}`.toUpperCase() || "Account"; } +/** + * The account's own mark, matching the web Limits bar: the provider glyph with + * its account initials badge. Replaces the position number that was hard to map + * to an account. + */ +function AccountAvatar({ account }: { readonly account: LimitAccount }) { + return ( + + ); +} + /** The spent share comes back at reset. SVG keeps the hatching static on both platforms. */ function AccountSegment({ remaining, @@ -123,13 +145,13 @@ function PoolWindowCard({ ) : null} - {pool.columns.map(({ account, window }, index) => { + {pool.columns.map(({ account, window }) => { if (!window) return ; return ( openAccount(account)} className="h-7 min-w-0 flex-1 overflow-hidden rounded-md bg-subtle" @@ -140,16 +162,14 @@ function PoolWindowCard({ pending={Boolean(window.resetsAt)} /> - - {index + 1} - + ); })} - {pool.columns.map(({ account, window }, index) => { + {pool.columns.map(({ account, window }) => { if (!window) return null; const credits = account.limits.resetCredits?.availableCount ?? 0; const resetsIn = formatResetsIn(window, now); @@ -157,16 +177,12 @@ function PoolWindowCard({ openAccount(account)} className="min-h-[44px] flex-row items-center gap-2 active:opacity-60" > - - - {index + 1} - - + ); } - return account.email ? : null; + return ; +} + +/** The account's label beside its avatar: the instance name when there is one, else the driver label. */ +function accountLabel(account: LimitAccount): string { + return account.displayName ?? getDriverOption(account.driver)?.label ?? String(account.driver); } /** @@ -114,11 +118,7 @@ function AccountName({ ); } - return ( - - {getDriverOption(account.driver)?.label ?? String(account.driver)} - - ); + return {accountLabel(account)}; } function Row({ label, children }: { readonly label: string; readonly children: ReactNode }) { @@ -165,9 +165,7 @@ function SegmentPopover({
- - {account.displayName ?? getDriverOption(account.driver)?.label ?? account.driver} - + {accountLabel(account)} {account.email ? ( - {index} +
+ {showAccountName ? ( - + + {accountLabel(account)} + ) : null} {remaining}% {/* Countdown and badge get their own plate: fill and hatching run under them otherwise. */} @@ -307,7 +305,7 @@ function PoolSegment({
- + {account.redeem ? ( } className="min-w-0 @2xl/pool:hidden" > - - - Segment - {index} - - + + {accountLabel(account)} {remaining}% {resetsIn?.replace("resets in ", "↻ ") ?? ""} @@ -445,9 +433,9 @@ function RedeemableSegmentPopup({ * the share of that account's quota still open. Equal widths are honest: every * account contributes the same share of the pool, whatever its plan. * - * Wide, each segment carries its own label. Narrow, the bar is a bare strip - * and a legend below lists the accounts in the same order; both open the - * same popover. + * Wide, each segment carries its own avatar and label. Narrow, the segment + * shows the avatar alone and a legend below repeats it with the label; both + * open the same popover. */ function PoolBar({ pool, diff --git a/packages/shared/src/usageLimits.test.ts b/packages/shared/src/usageLimits.test.ts index b2bef55cb108..ebe9f3d75f02 100644 --- a/packages/shared/src/usageLimits.test.ts +++ b/packages/shared/src/usageLimits.test.ts @@ -536,6 +536,18 @@ describe("pools", () => { expect(accounts[0]?.displayName).toBe("claude-team-seat"); }); + it("normalizes a hub account whose stripped id is empty to no display name", () => { + const hub = { + ...source, + accounts: [{ id: ".json", driver: claude, usageLimits: { checkedAt, windows: [window] } }], + }; + const input = new Map([ + [EnvironmentId.make("env-a"), { ...laptop, serverConfig: { usageLimitSources: [hub] } }], + ]); + const accounts = collectLimitAccounts(input); + expect(accounts[0]?.displayName ?? null).toBeNull(); + }); + it("pools windows by id across accounts and orders resets by when they land", () => { const input = new Map([ [ diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts index b1211bffd54d..2ce3f141d24f 100644 --- a/packages/shared/src/usageLimits.ts +++ b/packages/shared/src/usageLimits.ts @@ -261,7 +261,7 @@ export function collectLimitAccounts(presentations: LimitPresentations): readonl { key: `${source.id}:${account.id}`, driver: account.driver, - displayName: account.email ? null : account.id.replace(/\.json$/i, ""), + displayName: account.email ? null : account.id.replace(/\.json$/i, "") || null, email: account.email, plan: account.plan, accentColor: undefined,