fix(status-bar): keep usage reset countdowns live

This commit is contained in:
Brennan Benson
2026-07-20 13:37:49 -07:00
parent cd8fe48a9c
commit ec72d43538
2 changed files with 84 additions and 8 deletions
@@ -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 }) => <span data-agent-icon={agent} />
}))
vi.mock('@/hooks/useResetCountdownClock', () => ({
useResetCountdownClock: mocks.useResetCountdownClock
}))
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenuItem: ({
children,
onSelect: _onSelect,
...props
}: React.PropsWithChildren<{ onSelect?: () => void }>) => <div {...props}>{children}</div>
}))
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(
<UsageRow
@@ -28,6 +47,7 @@ describe('UsageRow', () => {
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(
<UsageRosterPanel
providers={[
{
...signedOutCodex,
session: {
usedPercent: 25,
windowMinutes: 300,
resetsAt: sessionReset,
resetDescription: null
},
weekly: {
usedPercent: 10,
windowMinutes: 10_080,
resetsAt: weeklyReset,
resetDescription: null
},
status: 'ok',
error: null
}
]}
display="used"
isRefreshing={false}
onRefresh={() => {}}
onOpenProvider={() => {}}
onSignIn={() => {}}
canSignIn={() => true}
onManageAccounts={() => {}}
onUsageDetails={() => {}}
/>
)
expect(mocks.useResetCountdownClock).toHaveBeenCalledOnce()
expect(mocks.useResetCountdownClock).toHaveBeenCalledWith([sessionReset, weeklyReset])
expect(markup).toContain('Resets in 2m')
})
})
@@ -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 (
<div className="flex min-w-0 flex-1 flex-col gap-1">
@@ -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 = (
<UsageRow p={p} display={display} state={state} showSignInAction={showSignInAction} />
<UsageRow
p={p}
display={display}
state={state}
showSignInAction={showSignInAction}
now={now}
/>
)
if (showSignInAction) {
return (