From 2e520d919baaa390d0704932b8c0fdad0d4a5cfd Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Tue, 15 Sep 2026 13:03:33 +0200 Subject: [PATCH 1/5] fix(usage): show each account's own mark in the Limits bar A pooled Limits bar identified accounts only by position number, so a row of 1,2,3,4 could not be mapped to an account without opening each popover. Reuse the account mark both clients already draw elsewhere (provider glyph with its accent initials badge, or the two-letter chip) in place of the number on the segment and its legend row, and before the name where the segment is wide enough to carry one. Web reuses AccountAvatar; mobile reuses ProviderInstanceIcon. No new component or state. Mobile's ProviderInstanceIcon takes an optional surfaceColor because the usage page cannot read the theme for the badge ring. --- apps/mobile/src/components/ProviderIcon.tsx | 5 +- .../src/features/usage/UsageLimitsPooled.tsx | 37 ++++++++----- .../components/usage/UsageLimitsPooled.tsx | 55 +++++++------------ 3 files changed, 48 insertions(+), 49 deletions(-) diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index 964c60ebcbe2..f694a9532587 100644 --- a/apps/mobile/src/components/ProviderIcon.tsx +++ b/apps/mobile/src/components/ProviderIcon.tsx @@ -169,7 +169,8 @@ export function ProviderInstanceIcon(props: { readonly displayName: string; readonly accentColor?: string; readonly showBadge?: boolean; - readonly surfaceColor: string; + /** The surface the badge ring blends into; omit when the caller cannot name it. */ + readonly surfaceColor?: string; }) { return ( @@ -188,7 +189,7 @@ export function ProviderInstanceIcon(props: { paddingHorizontal: 2, borderRadius: 999, borderWidth: 1, - borderColor: props.surfaceColor, + borderColor: props.surfaceColor ?? "transparent", backgroundColor: props.accentColor, alignItems: "center", justifyContent: "center", diff --git a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx index 713b9ffd160f..fd3f5d65e9ec 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,23 @@ 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 +140,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 +157,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 +172,12 @@ function PoolWindowCard({ openAccount(account)} className="min-h-[44px] flex-row items-center gap-2 active:opacity-60" > - - - {index + 1} - - + : null; } +/** 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); +} + /** * Who an account is, without printing the email: the instance name when there * is one, else a two-letter chip. The address itself is revealed on demand in @@ -114,11 +117,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 +164,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 +304,7 @@ function PoolSegment({
- + {account.redeem ? ( } className="min-w-0 @2xl/pool:hidden" > - - - Segment - {index} - - + + {accountLabel(account)} {remaining}% {resetsIn?.replace("resets in ", "↻ ") ?? ""} @@ -445,9 +432,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, From bc95158bf2780da724ed70dbac17ffbd9ca5d48e Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Tue, 15 Sep 2026 13:18:03 +0200 Subject: [PATCH 2/5] fix(usage): keep Limits account marks distinct and never blank Macroscope review: a native account without a displayName drew the same glyph and driver label as its sibling, and a hub account with neither an instance nor an email drew nothing in the narrow segment. Carry the email into the badge initials so same-driver accounts differ, and fall back to the position number only when there is no mark to draw. --- apps/web/src/components/usage/UsageLimitsPooled.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/usage/UsageLimitsPooled.tsx b/apps/web/src/components/usage/UsageLimitsPooled.tsx index 603ad396575d..4d226472df4d 100644 --- a/apps/web/src/components/usage/UsageLimitsPooled.tsx +++ b/apps/web/src/components/usage/UsageLimitsPooled.tsx @@ -80,9 +80,9 @@ function AccountAvatar({ return ( - + {account.redeem || account.email ? : index}
From 6f1726d358672d02163d7bac9c636a5489535fbc Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Thu, 17 Sep 2026 05:54:10 +0200 Subject: [PATCH 3/5] fix(usage): always draw a Limits account mark, even with no email CodeRabbit: AccountAvatar returned null for an account with a display name but no email and no redeem target, and for a hub account whose stripped id is empty, so the narrow segment kept its position number and the label could render blank. Render the provider glyph for any account without an email, and normalize an empty stripped hub id to null so the driver label shows. --- apps/web/src/components/usage/UsageLimitsPooled.tsx | 9 +++++---- packages/shared/src/usageLimits.test.ts | 12 ++++++++++++ packages/shared/src/usageLimits.ts | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/usage/UsageLimitsPooled.tsx b/apps/web/src/components/usage/UsageLimitsPooled.tsx index 4d226472df4d..746d860a45b5 100644 --- a/apps/web/src/components/usage/UsageLimitsPooled.tsx +++ b/apps/web/src/components/usage/UsageLimitsPooled.tsx @@ -67,7 +67,8 @@ function AccountChip({ email }: { readonly email: string }) { /** * The same mark the model picker uses for a native instance (provider glyph, - * initials badge, accent); hub accounts have no instance, so they get the chip. + * initials badge, accent). A hub account with an email and no instance gets the + * two-letter chip; anything else still gets the glyph, so no account is blank. */ function AccountAvatar({ account, @@ -76,7 +77,7 @@ function AccountAvatar({ readonly account: LimitAccount; readonly className?: string; }) { - if (account.redeem) { + if (!account.email || account.redeem) { return ( ); } - return account.email ? : null; + return ; } /** The account's label beside its avatar: the instance name when there is one, else the driver label. */ @@ -275,7 +276,7 @@ function PoolSegment({ aria-hidden className="absolute inset-0 flex items-center justify-center @2xl/pool:hidden" > - {account.redeem || account.email ? : index} +
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, From bd8aa582765243292c1a9056a34fe83fd61a5d85 Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Mon, 5 Oct 2026 07:01:28 +0200 Subject: [PATCH 4/5] fix(mobile): Limits badges take initials from the full email, as on web Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/mobile/src/features/usage/UsageLimitsPooled.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx index fd3f5d65e9ec..9f8aa3f815fd 100644 --- a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx +++ b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx @@ -47,7 +47,9 @@ function AccountAvatar({ account }: { readonly account: LimitAccount }) { From e24e69e42d9ec1d572969817c1f3b241d2b3f39c Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Mon, 5 Oct 2026 07:46:12 +0200 Subject: [PATCH 5/5] fix(mobile): Limits badges pass a transparent ring instead of loosening ProviderInstanceIcon Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/mobile/src/components/ProviderIcon.tsx | 5 ++--- apps/mobile/src/features/usage/UsageLimitsPooled.tsx | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index f694a9532587..964c60ebcbe2 100644 --- a/apps/mobile/src/components/ProviderIcon.tsx +++ b/apps/mobile/src/components/ProviderIcon.tsx @@ -169,8 +169,7 @@ export function ProviderInstanceIcon(props: { readonly displayName: string; readonly accentColor?: string; readonly showBadge?: boolean; - /** The surface the badge ring blends into; omit when the caller cannot name it. */ - readonly surfaceColor?: string; + readonly surfaceColor: string; }) { return ( @@ -189,7 +188,7 @@ export function ProviderInstanceIcon(props: { paddingHorizontal: 2, borderRadius: 999, borderWidth: 1, - borderColor: props.surfaceColor ?? "transparent", + borderColor: props.surfaceColor, backgroundColor: props.accentColor, alignItems: "center", justifyContent: "center", diff --git a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx index 9f8aa3f815fd..b4e0a99d762e 100644 --- a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx +++ b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx @@ -52,6 +52,9 @@ function AccountAvatar({ account }: { readonly account: LimitAccount }) { displayName={account.displayName ?? account.email ?? accountName(account)} accentColor={account.accentColor} showBadge={Boolean(account.displayName ?? account.email)} + // The badge sits on hatched segments and list rows, with no single + // background for its ring to match. + surfaceColor="transparent" /> ); }