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 (