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(() => {}); + }} /> )}