From ec72d43538856ed1dbcd164aef436e96ca33c2c7 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:37:49 -0700 Subject: [PATCH] fix(status-bar): keep usage reset countdowns live --- .../status-bar/UsageRosterPanel.test.tsx | 65 ++++++++++++++++++- .../status-bar/UsageRosterPanel.tsx | 27 ++++++-- 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx b/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx index 54c8e1fc85e..c6a2d115a61 100644 --- a/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx +++ b/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx @@ -1,15 +1,30 @@ import { renderToStaticMarkup } from 'react-dom/server' -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ProviderRateLimits } from '../../../../shared/rate-limit-types' +const mocks = vi.hoisted(() => ({ + now: 1_000_000_000, + useResetCountdownClock: vi.fn(() => 1_000_000_000) +})) + vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback })) vi.mock('@/lib/agent-catalog', () => ({ AgentIcon: ({ agent }: { agent: string }) => })) +vi.mock('@/hooks/useResetCountdownClock', () => ({ + useResetCountdownClock: mocks.useResetCountdownClock +})) +vi.mock('@/components/ui/dropdown-menu', () => ({ + DropdownMenuItem: ({ + children, + onSelect: _onSelect, + ...props + }: React.PropsWithChildren<{ onSelect?: () => void }>) =>
{children}
+})) -import { UsageRow } from './UsageRosterPanel' +import { UsageRosterPanel, UsageRow } from './UsageRosterPanel' const signedOutCodex: ProviderRateLimits = { provider: 'codex', @@ -21,6 +36,10 @@ const signedOutCodex: ProviderRateLimits = { } describe('UsageRow', () => { + beforeEach(() => { + mocks.useResetCountdownClock.mockClear() + }) + it('renders sign-in as row copy instead of nesting an interactive button', () => { const markup = renderToStaticMarkup( { display="used" state={{ kind: 'sign-in', statusLabel: 'not signed in' }} showSignInAction + now={mocks.now} /> ) @@ -53,6 +73,7 @@ describe('UsageRow', () => { display="remaining" state={{ kind: 'usage', statusLabel: null }} showSignInAction={false} + now={mocks.now} /> ) @@ -60,4 +81,44 @@ describe('UsageRow', () => { expect(markup).toContain('width:75%') expect(markup).not.toContain('width:25%') }) + + it('uses one shared clock for live reset labels across the roster', () => { + const sessionReset = mocks.now + 2 * 60_000 + const weeklyReset = mocks.now + 7 * 24 * 60 * 60_000 + const markup = renderToStaticMarkup( + {}} + onOpenProvider={() => {}} + onSignIn={() => {}} + canSignIn={() => true} + onManageAccounts={() => {}} + onUsageDetails={() => {}} + /> + ) + + expect(mocks.useResetCountdownClock).toHaveBeenCalledOnce() + expect(mocks.useResetCountdownClock).toHaveBeenCalledWith([sessionReset, weeklyReset]) + expect(markup).toContain('Resets in 2m') + }) }) diff --git a/src/renderer/src/components/status-bar/UsageRosterPanel.tsx b/src/renderer/src/components/status-bar/UsageRosterPanel.tsx index 21180a733c4..2d441896108 100644 --- a/src/renderer/src/components/status-bar/UsageRosterPanel.tsx +++ b/src/renderer/src/components/status-bar/UsageRosterPanel.tsx @@ -1,6 +1,7 @@ import React from 'react' import { ChevronRight, RefreshCw } from 'lucide-react' import { DropdownMenuItem } from '@/components/ui/dropdown-menu' +import { useResetCountdownClock } from '@/hooks/useResetCountdownClock' import { translate } from '@/i18n/i18n' import { formatRateLimitWindowChipLabel, formatWindowLabel } from '@/lib/window-label-formatter' import type { ProviderRateLimits, RateLimitWindow } from '../../../../shared/rate-limit-types' @@ -63,14 +64,14 @@ export function getTightestUsageSection(p: ProviderRateLimits): UsageSection | n } // The soonest-resetting window summarizes the agent's next reset in one line. -function soonestResetLabel(sections: UsageSection[]): string | null { +function soonestResetLabel(sections: UsageSection[], now: number): string | null { const resets = sections .map((s) => s.window.resetsAt) - .filter((r): r is number => typeof r === 'number') + .filter((r): r is number => typeof r === 'number' && Number.isFinite(r)) if (resets.length === 0) { return null } - return formatResetCountdown(Math.min(...resets) - Date.now()) + return formatResetCountdown(Math.min(...resets) - now) } // Presentational row: a compact header (icon · name · plan · reset) with @@ -81,18 +82,20 @@ export function UsageRow({ p, display, state, - showSignInAction + showSignInAction, + now }: { p: ProviderRateLimits display: UsagePercentageDisplay state: UsageRosterRowState showSignInAction: boolean + now: number }): React.JSX.Element { const sections = usedSections(p) const hasUsage = sections.length > 0 const name = getProviderDisplayName(p.provider) const plan = formatPlanLabel(p.planType) - const reset = hasUsage ? soonestResetLabel(sections) : null + const reset = hasUsage ? soonestResetLabel(sections, now) : null return (
@@ -176,6 +179,12 @@ export function UsageRosterPanel({ // default clickable row. renderRow?: (p: ProviderRateLimits, row: React.ReactNode) => React.ReactNode }): React.JSX.Element { + // Why: one boundary-scheduled clock keeps every open row current without per-provider timers. + const now = useResetCountdownClock( + providers.flatMap((provider) => + usedSections(provider).map((section) => section.window.resetsAt) + ) + ) // Worst-first so the agent nearest a limit sits on top. const sorted = [...providers].sort( (a, b) => providerMaxUsed(usedSections(b)) - providerMaxUsed(usedSections(a)) @@ -211,7 +220,13 @@ export function UsageRosterPanel({ const state = getUsageRosterRowState(p, usedSections(p).length > 0) const showSignInAction = state.kind === 'sign-in' && canSignIn(p.provider) const rowNode = ( - + ) if (showSignInAction) { return (