mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
fix(status-bar): keep usage reset countdowns live
This commit is contained in:
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user