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