From a8a9084dcd6431750e8c68c19135e0f908a79d0b Mon Sep 17 00:00:00 2001 From: Mohamed Mohsen Date: Sun, 30 Aug 2026 04:43:57 +0300 Subject: [PATCH] Show Codex account reset times --- .../src/components/CodexAccountsMenu.test.tsx | 44 ++++- .../src/components/CodexAccountsMenu.tsx | 175 ++++++++++++------ .../desktop-tauri/src/components/MenuCard.tsx | 5 +- apps/desktop-tauri/src/styles.css | 18 ++ 4 files changed, 180 insertions(+), 62 deletions(-) diff --git a/apps/desktop-tauri/src/components/CodexAccountsMenu.test.tsx b/apps/desktop-tauri/src/components/CodexAccountsMenu.test.tsx index 607acd0221..82e56885bd 100644 --- a/apps/desktop-tauri/src/components/CodexAccountsMenu.test.tsx +++ b/apps/desktop-tauri/src/components/CodexAccountsMenu.test.tsx @@ -40,14 +40,17 @@ function account(id: string, extra: Partial = {}): CodexAccount { }; } -function snapshot(usedPercent: number): CodexAccountUsageSnapshot { +function snapshot( + usedPercent: number, + resetAt: string | null = null, +): CodexAccountUsageSnapshot { return { email: "user@example.com", providerAccountId: null, plan: "free", allowed: true, limitReached: false, - primaryWindow: { usedPercent, resetAt: null, limitWindowSeconds: 3600 }, + primaryWindow: { usedPercent, resetAt, limitWindowSeconds: 18_000 }, secondaryWindow: null, credits: null, updatedAt: "2024-01-01T00:00:00Z", @@ -57,12 +60,19 @@ function snapshot(usedPercent: number): CodexAccountUsageSnapshot { // Wrap the component so the `t` from useLocale is a stable identity that just // returns the key (the component uses `t(key)` for locale strings and a badge // label; returning the key is enough to assert rendering). -function renderMenu(hideEmail: boolean, state: CodexAccountsStateBridge) { +function renderMenu( + hideEmail: boolean, + state: CodexAccountsStateBridge, + resetTimeRelative = true, +) { tauriMocks.getCodexAccountsState.mockResolvedValue(state); tauriMocks.getLocaleStrings.mockResolvedValue(buildBundle({})); return render( - + , ); } @@ -141,6 +151,30 @@ describe("CodexAccountsMenu", () => { expect((fills[0] as HTMLElement).style.width).toBe("42%"); }); + it("shows the five-hour usage and local reset time for each account", async () => { + const resetAt = "2030-01-02T03:04:00Z"; + const expectedReset = new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }).format(new Date(resetAt)); + + renderMenu( + false, + { + accounts: [account("1", { source: "ambient" }), account("2")], + snapshots: { "1": snapshot(30, resetAt), "2": snapshot(70, resetAt) }, + }, + false, + ); + + await screen.findByText("user-1@example.com"); + expect(screen.getAllByText("5h")).toHaveLength(2); + expect(screen.getByText("30% PanelUsedSuffix")).toBeDefined(); + expect(screen.getAllByText(`MetricResetsIn ${expectedReset}`)).toHaveLength(2); + }); + it("switches an account and kicks a provider refresh", async () => { renderMenu(false, { accounts: [account("1", { source: "ambient" }), account("2")], @@ -162,4 +196,4 @@ describe("CodexAccountsMenu", () => { expect(tauriMocks.codexAccountSwitch).toHaveBeenCalledWith("2"); expect(tauriMocks.refreshProviders).toHaveBeenCalledTimes(1); }); -}); \ No newline at end of file +}); diff --git a/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx b/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx index aa2b8f9ff9..a04fd6f79c 100644 --- a/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx +++ b/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx @@ -6,6 +6,7 @@ import type { CodexAccountUsageSnapshot, } from "../types/bridge"; import { useLocale } from "../hooks/useLocale"; +import { useFormattedResetTime } from "../hooks/useFormattedResetTime"; import { maskEmail } from "./MenuCard"; import { codexAccountSwitch, @@ -22,7 +23,13 @@ import { * Switch action. Switching updates the ambient identity and triggers a * provider refresh so the tray icon/menu reflect the now-active account. */ -export default function CodexAccountsMenu({ hideEmail }: { hideEmail: boolean }) { +export default function CodexAccountsMenu({ + hideEmail, + resetTimeRelative, +}: { + hideEmail: boolean; + resetTimeRelative: boolean; +}) { const { t } = useLocale(); const [accounts, setAccounts] = useState([]); const [snapshots, setSnapshots] = useState< @@ -91,65 +98,121 @@ export default function CodexAccountsMenu({ hideEmail }: { hideEmail: boolean }) )}
    - {accounts.map((account) => { - const snapshot = snapshots[account.id]; - // Prefer the primary (session) window, but accounts whose backend - // only returns a weekly window have primaryWindow: null — fall back - // to the next filled window in canonical order (primary → - // secondary; the account-snapshot bridge carries no tertiary or - // extra rate windows) so the usage bar still renders. - const usageWindow = - snapshot?.primaryWindow ?? snapshot?.secondaryWindow ?? null; - const pct = usageWindow - ? Math.round(usageWindow.usedPercent) - : null; - const label = - account.nickname ?? - account.emailHint ?? - account.authSubject ?? - shrink(account.id); - const shown = hideEmail ? maskEmail(label) : label; - const isAmbient = account.source === "ambient"; - return ( -
  • -
    -
    - - {shown} - {isAmbient && ( - - {t("CodexAccountsSourceAmbient")} - - )} - - {pct !== null && ( - - - - )} -
    - -
    -
  • - ); - })} + {accounts.map((account) => ( + + ))}
); } +function CodexAccountRow({ + account, + snapshot, + hideEmail, + resetTimeRelative, + busy, + onSwitch, +}: { + account: CodexAccount; + snapshot: CodexAccountUsageSnapshot | undefined; + hideEmail: boolean; + resetTimeRelative: boolean; + busy: boolean; + onSwitch: (id: string) => Promise; +}) { + const { t } = useLocale(); + // Prefer the primary (normally five-hour) window. Accounts whose backend + // only returns a weekly window have primaryWindow: null, so keep the + // existing secondary-window fallback for their bar and reset detail. + const usageWindow = + snapshot?.primaryWindow ?? snapshot?.secondaryWindow ?? null; + const pct = usageWindow ? Math.round(usageWindow.usedPercent) : null; + const resetText = useFormattedResetTime( + usageWindow?.resetAt ?? null, + null, + resetTimeRelative, + ); + const resetLabel = resetText + ? resetTimeRelative + ? resetText + : `${t("MetricResetsIn")} ${resetText}` + : null; + const windowLabel = formatWindowLabel(usageWindow?.limitWindowSeconds); + const label = + account.nickname ?? + account.emailHint ?? + account.authSubject ?? + shrink(account.id); + const shown = hideEmail ? maskEmail(label) : label; + const isAmbient = account.source === "ambient"; + + return ( +
  • +
    +
    + + {shown} + {isAmbient && ( + + {t("CodexAccountsSourceAmbient")} + + )} + + {(pct !== null || resetLabel) && ( + + {windowLabel && {windowLabel}} + {pct !== null && ( + {pct}% {t("PanelUsedSuffix")} + )} + {resetLabel && {resetLabel}} + + )} + {pct !== null && ( + + + + )} +
    + +
    +
  • + ); +} + +function formatWindowLabel( + limitWindowSeconds: number | null | undefined, +): string | null { + if (!limitWindowSeconds || limitWindowSeconds <= 0) return null; + if (limitWindowSeconds % 86_400 === 0) { + return `${limitWindowSeconds / 86_400}d`; + } + if (limitWindowSeconds % 3_600 === 0) { + return `${limitWindowSeconds / 3_600}h`; + } + return null; +} + function shrink(id: string): string { return id.length <= 12 ? id : `${id.slice(0, 8)}…`; -} \ No newline at end of file +} diff --git a/apps/desktop-tauri/src/components/MenuCard.tsx b/apps/desktop-tauri/src/components/MenuCard.tsx index e879e8fff5..46aa3fec14 100644 --- a/apps/desktop-tauri/src/components/MenuCard.tsx +++ b/apps/desktop-tauri/src/components/MenuCard.tsx @@ -321,7 +321,10 @@ export default function MenuCard({ )} {provider.providerId === "codex" && ( - + )} ); diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css index 960fdbf4bb..bfc85bdd2e 100644 --- a/apps/desktop-tauri/src/styles.css +++ b/apps/desktop-tauri/src/styles.css @@ -4017,6 +4017,24 @@ html:has(.menu-surface--tray) { color: var(--provider-status-ok, #4ade80); } +.codex-menu-accounts__usage { + display: flex; + align-items: center; + gap: 0; + min-width: 0; + font-size: 0.66rem; + color: var(--provider-row-text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.codex-menu-accounts__usage > span + span::before { + content: "·"; + margin: 0 5px; + opacity: 0.65; +} + .codex-menu-accounts__bar { display: block; height: 4px;