From b10a49c0826cbc70528e058d2660c432e98ba814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 21 Aug 2026 21:33:58 +0000 Subject: [PATCH] an account with no saved sign-in gets a chip you can read, not one you can use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/apps/Chat/components/UsagePopover.tsx | 153 ++++++++++++------ 1 file changed, 103 insertions(+), 50 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx b/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx index 04552754..3af91bb1 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx @@ -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; }) => (
- {accounts.map((account) => { - const busy = isSwitching && switchingTo === account.accountUuid; + {chips.map((chip) => { + const looking = chip.kind === 'view' && viewing === chip.email; return ( ); })} @@ -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(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'}
)} - {unreachable > 0 && ( -
- {unreachable} older account{unreachable === 1 ? '' : 's'} with no saved sign-in -
+ {/* 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 && ( +
No saved sign-in — run /login to use this one
)} {/* 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) { )} - {switchable.length > 1 && ( - 1 && ( + 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(() => {}); + }} /> )}