an account with no saved sign-in gets a chip you can read, not one you can use

Three chip states now, and the third is a different question rather than a broken version of the second:

  filled          the account you are
  plain           an account you can become — click switches
  dashed outline  an account you can only look at — click shows its last known usage

"Has my other allowance come back yet" is answerable from a snapshot, and the machinery for it was already
right: a reset time is an absolute instant, and currentLimit rolls a past window forward and zeroes it
instead of showing a stale percentage. Phase one was hiding a correct answer.

What you cannot do is act on it without a /login, so the chip says that before it is clicked (in its title
and in its border) and the panel says it again while you are reading one. Naming the fix rather than the
problem: "No saved sign-in — run /login to use this one".

Viewing is deliberately NOT a general tab selection. Only an account with no credential can be viewed,
because for any other one the useful gesture is switching to it and then reading live numbers — which keeps
phase one's invariant intact: if the panel shows a signed-in account, it is THE signed-in account. Switching
clears the view for the same reason, so the numbers and the chips can never disagree.

Replaces the count line, which was a fact about somewhere else. The staleness line that was already in this
file — "Not signed in · as of 20 minutes ago" — now finally has a case that reaches it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 21:33:58 +00:00
co-authored by Claude Opus 5
parent 4b2cb3d266
commit b10a49c082
@@ -132,61 +132,67 @@ const Models = ({ account }: { account: UsageAccount }) => {
};
/**
* One chip per Anthropic account, and clicking one BECOMES it.
* A chip per Anthropic account this machine has seen, and clicking one does one of two things.
*
* This used to select which account's stored numbers you were reading, which turned out to be the less
* useful half of the same gesture: having established that the other account has room, what you want is to
* use it. So the chip is now an action, and the panel below it always shows the account you are actually
* signed into.
* switch — a stored credential exists, so the click BECOMES that account
* view — only a stored reading exists, so the click shows its cached numbers and nothing changes
*
* The spinner is not decoration. A switch rewrites a credential on disk and changes nothing you can see —
* without an indicator the honest reading of a click is "nothing happened", and the second click is somebody
* checking.
* The second kind is not a degraded version of the first, it is a different question. An account whose
* sign-in is gone still has percentages and a reset time worth reading — "has my other allowance come back
* yet" is answerable from a snapshot, because a reset time is an absolute instant and `currentLimit` rolls a
* past window forward rather than showing a stale one. What you cannot do is act on the answer without a
* `/login`, and that is what the chip has to make obvious BEFORE it is clicked.
*
* So: filled for the account you are, plain for one you can become, and dashed-outline for one you can only
* look at.
*/
const AccountSwitcher = ({
accounts,
type Chip = {
key: string;
email: string;
title: string;
kind: 'current' | 'switch' | 'view';
busy: boolean;
};
const AccountChips = ({
chips,
isSwitching,
switchingTo,
onSwitch,
viewing,
onClick,
}: {
accounts: SwitchableAccount[];
chips: Chip[];
isSwitching: boolean;
switchingTo: string | null;
onSwitch: (accountUuid: string) => void;
viewing: string | null;
onClick: (chip: Chip) => void;
}) => (
<div className="flex flex-wrap items-center gap-1 border-t border-border px-2 py-2">
{accounts.map((account) => {
const busy = isSwitching && switchingTo === account.accountUuid;
{chips.map((chip) => {
const looking = chip.kind === 'view' && viewing === chip.email;
return (
<button
key={account.accountUuid}
key={chip.key}
type="button"
// An expired refresh token cannot be switched to, so the chip says why instead of failing on click.
disabled={!account.usable || isSwitching || account.current}
onClick={() => onSwitch(account.accountUuid)}
title={
account.current
? `${account.emailAccount} — signed in`
: account.usable
? `Switch to ${account.emailAccount}${account.orgName ? ` (${account.orgName})` : ''}`
: `${account.emailAccount} — sign-in expired, run /login`
}
disabled={isSwitching || (chip.kind === 'current' && !viewing)}
onClick={() => onClick(chip)}
title={chip.title}
className={`inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors ${
account.current
chip.kind === 'current' && !viewing
? 'bg-primary text-primary-foreground'
: account.usable
? 'cursor-pointer text-muted-foreground hover:bg-foreground/10 hover:text-foreground'
: 'text-muted-foreground/40 line-through'
} ${isSwitching && !busy ? 'opacity-50' : ''}`}
: chip.kind === 'view'
? // Dashed and unfilled: the border says "not a real destination" without greying it out to
// the point of looking broken, because it IS clickable — just not for switching.
`cursor-pointer border border-dashed border-border ${
looking ? 'bg-foreground/10 text-foreground' : 'text-muted-foreground/70 hover:text-foreground'
}`
: 'cursor-pointer text-muted-foreground hover:bg-foreground/10 hover:text-foreground'
} ${isSwitching && !chip.busy ? 'opacity-50' : ''}`}
>
{busy && <Loader2 className="h-3 w-3 animate-spin" />}
{chip.busy && <Loader2 className="h-3 w-3 animate-spin" />}
{/* The WHOLE address. This showed the local part alone, on the reasoning that the domain is what
accounts have in common — which is wrong for the case that matters: a personal and a work
account belonging to one person are `andre.padez@gmail.com` and `andre.padez@pertento.ai`, so
the local part is the half they SHARE and the domain is the half that tells them apart. Two
chips read as the same account. The strip wraps instead of scrolling, so a long address is
readable rather than clipped off the edge. */}
{account.emailAccount}
account belonging to one person share the local part, so the domain is the half that tells
them apart. Two chips read as the same account. */}
{chip.email}
</button>
);
})}
@@ -241,7 +247,21 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) {
A selection would let the numbers and the chips disagree — reading one account's percentages with
another's chip lit — and that disagreement is the exact confusion this whole change set out to remove.
*/
const selected = accounts[0] ?? null;
/*
`viewing` is null for the normal case — the account you are signed into — and an email when you have
clicked an archived chip to read its cached numbers.
It is deliberately NOT a general tab selection. Only accounts with no stored credential can be viewed,
because for any other one the useful gesture is switching to it and then reading live numbers. That keeps
the invariant that made phase one worth doing: if the panel shows a signed-in account, it is THE
signed-in account.
*/
const [viewing, setViewing] = useState<string | null>(null);
useEffect(() => {
if (!open) setViewing(null);
}, [open]);
const selected = (viewing ? allAccounts.find((a) => a.email === viewing) : accounts[0]) ?? accounts[0] ?? null;
/*
Accounts with a stored READING but no stored credential — an old login that can be looked at and not
returned to. That is what is worth saying out loud.
@@ -249,7 +269,28 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) {
This counted `allAccounts - accounts` until the switcher existed, which then double-counted: it called
an account "not shown" while its chip sat two lines below.
*/
const unreachable = allAccounts.filter((a) => !switchable.some((s) => s.emailAccount === a.email)).length;
const archived = allAccounts.filter((a) => !switchable.some((s) => s.emailAccount === a.email));
const chips: Chip[] = [
...switchable.map((a) => ({
key: a.accountUuid,
email: a.emailAccount,
kind: (a.current ? 'current' : 'switch') as Chip['kind'],
busy: isSwitching && switchingTo === a.accountUuid,
title: a.current
? `${a.emailAccount} — signed in`
: a.usable
? `Switch to ${a.emailAccount}${a.orgName ? ` (${a.orgName})` : ''}`
: `${a.emailAccount} — sign-in expired, run /login`,
})),
...archived.map((a) => ({
key: `archived:${a.email}`,
email: a.email,
kind: 'view' as Chip['kind'],
busy: false,
title: `${a.email} — no saved sign-in. Click to read its last known usage; run /login to use it.`,
})),
];
// The countdowns tick. Only while the panel is open — an interval running behind a closed popover is a
// re-render a minute for something nobody is looking at.
@@ -298,10 +339,11 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) {
{switchError instanceof Error ? switchError.message : 'could not switch account'}
</div>
)}
{unreachable > 0 && (
<div className="mt-0.5 text-[10px] text-muted-foreground">
{unreachable} older account{unreachable === 1 ? '' : 's'} with no saved sign-in
</div>
{/* Said on the account being LOOKED AT rather than as a count somewhere else. The numbers below are
real and the reset times are still correct; the only thing missing is the ability to act on
them, and naming the fix beats naming the problem. */}
{viewing && (
<div className="mt-0.5 text-[10px] text-warning">No saved sign-in — run /login to use this one</div>
)}
{/*
How old this reading is, and it earns its line only when it is not live.
@@ -320,12 +362,23 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) {
)}
</div>
{switchable.length > 1 && (
<AccountSwitcher
accounts={switchable}
{chips.length > 1 && (
<AccountChips
chips={chips}
isSwitching={isSwitching}
switchingTo={switchingTo}
onSwitch={(uuid) => void switchTo(uuid).catch(() => {})}
viewing={viewing}
onClick={(chip) => {
if (chip.kind === 'view') {
setViewing(chip.email);
return;
}
// Switching returns the panel to live numbers: the account you just became is the one whose
// percentages you want, and leaving an archived snapshot on screen after a switch is how the
// numbers and the chips end up disagreeing.
setViewing(null);
const target = switchable.find((a) => a.emailAccount === chip.email);
if (target && !target.current) void switchTo(target.accountUuid).catch(() => {});
}}
/>
)}