feat(status-bar): consolidate agent usage into a single roster popover

The footer usage cluster is now one quiet, borderless control: each agent
shows its tightest window as neutral text (letter badges at icon-only
width), and clicking anywhere opens a consolidated Usage popover listing
every agent worst-first — icon, name, plan, soonest reset, and per-window
threshold-colored bars. Claude/Codex rows drill into their existing
account switcher, runtime toggle, and Codex reset credits via a submenu
variant of ProviderDetailsMenu; all other providers drill into their
detail panel. Row actions and the footer links route to Settings.

Also: barColor's <60% band goes green -> neutral so color is reserved for
approaching limits (threshold color now lives only in the popover, the
always-visible bar stays monochrome), Codex plan_type is surfaced as
ProviderRateLimits.planType for the "Codex · Plus" label, and the Fable
weekly window is labeled "Fable" so it no longer collides with "wk".
This commit is contained in:
Brennan Benson
2026-07-14 13:22:50 -07:00
parent 5be44d1553
commit 97cd05b02e
13 changed files with 604 additions and 137 deletions
+2
View File
@@ -544,6 +544,8 @@ async function fetchViaBackend(
provider: 'codex',
session: mapBackendUsageWindow(payload.rate_limit?.primary_window, 300),
weekly: mapBackendUsageWindow(payload.rate_limit?.secondary_window, 10080),
// Surfaced for the status-bar Usage row (e.g. "Codex · Plus").
planType: payload.plan_type,
...(payload.rate_limit_reset_credits !== undefined
? {
rateLimitResetCredits:
@@ -33,6 +33,9 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useAppStore } from '../../store'
@@ -53,11 +56,13 @@ import {
barColor,
clampUsedPercent,
formatResetCreditExpiry,
getProviderDisplayName,
getProviderUsageStatusLabel
} from './tooltip'
import { ClaudeIcon, GeminiIcon, MiniMaxIcon, OpenAIIcon, OpenCodeGoIcon } from './icons'
import { AgentIcon } from '@/lib/agent-catalog'
import { formatWindowLabel } from '@/lib/window-label-formatter'
import { UsageRosterPanel } from './UsageRosterPanel'
import { markLiveCodexSessionsForRestart } from '@/lib/codex-session-restart'
import { UpdateStatusSegment } from './UpdateStatusSegment'
import { isStatusBarItemAvailable } from './status-bar-agent-gating'
@@ -81,6 +86,7 @@ import {
} from '@/runtime/runtime-provider-accounts-client'
import { translate } from '@/i18n/i18n'
import {
getDisplayedUsagePercentage,
normalizeUsagePercentageDisplay,
type UsagePercentageDisplay
} from '../../../../shared/usage-percentage-display'
@@ -660,14 +666,20 @@ function AccountRuntimeToggle<TGroup extends { key: string; label: string }>({
)
}
function ClaudeSwitcherMenu({
// Exported so its account-switch/reset logic is preserved for row drill-in even
// though the footer now opens the consolidated UsageRosterPanel first.
export function ClaudeSwitcherMenu({
claude,
compact,
iconOnly
iconOnly,
asSubmenu = false,
triggerContent
}: {
claude: ProviderRateLimits
compact: boolean
iconOnly: boolean
asSubmenu?: boolean
triggerContent?: React.ReactNode
}): React.JSX.Element {
const [open, setOpen] = useState(false)
const [accountsExpanded, setAccountsExpanded] = useState(false)
@@ -832,6 +844,8 @@ function ClaudeSwitcherMenu({
provider={claude}
compact={compact}
iconOnly={iconOnly}
asSubmenu={asSubmenu}
triggerContent={triggerContent}
ariaLabel={translate(
'auto.components.status.bar.StatusBar.3dd7ddfae1',
'Open Claude details and account switcher'
@@ -943,21 +957,6 @@ function ClaudeSwitcherMenu({
)
}
// ---------------------------------------------------------------------------
// Mini progress bar (shows consumption / % used, grey)
// ---------------------------------------------------------------------------
function MiniBar({ usedPct }: { usedPct: number }): React.JSX.Element {
return (
<div className="w-[48px] h-[6px] rounded-full bg-muted overflow-hidden flex-shrink-0">
<div
className="h-full rounded-full transition-all duration-300 bg-muted-foreground/40"
style={{ width: `${clampUsedPercent(usedPct)}%` }}
/>
</div>
)
}
// ---------------------------------------------------------------------------
// Inline usage bars (compact bars for inactive accounts in the switcher)
// ---------------------------------------------------------------------------
@@ -1093,19 +1092,43 @@ function InlineUsageSkeleton(): React.JSX.Element {
function WindowLabel({
w,
label,
display
display,
showLabel = true
}: {
w: RateLimitWindow
label: string
display: UsagePercentageDisplay
showLabel?: boolean
}): React.JSX.Element {
return (
<span className="tabular-nums">
{formatUsagePercentageLabel(w.usedPercent, display)} {label}
{getDisplayedUsagePercentage(w.usedPercent, display)}%{showLabel ? ` ${label}` : ''}
</span>
)
}
// Single-letter provider badge for the icon-only (narrow) status bar.
function getProviderLetter(provider: ProviderRateLimits['provider']): string {
switch (provider) {
case 'claude':
return 'C'
case 'gemini':
return 'G'
case 'opencode-go':
return 'O'
case 'kimi':
return 'K'
case 'antigravity':
return 'A'
case 'minimax':
return 'M'
case 'grok':
return 'R'
case 'codex':
return 'X'
}
}
// ---------------------------------------------------------------------------
// Provider segment
// ---------------------------------------------------------------------------
@@ -1171,24 +1194,27 @@ function ProviderSegment({
if (p.buckets && p.buckets.length > 0) {
const visibleBuckets = p.buckets.filter((b) => STATUS_BAR_BUCKET_NAMES.has(b.name))
// Roster shows the tightest bucket (highest % used) to stay compact.
const tightestBucket =
visibleBuckets.length > 0
? visibleBuckets.reduce((a, b) => (b.usedPercent > a.usedPercent ? b : a))
: null
return (
<span className="inline-flex items-center gap-1.5">
<ProviderIcon provider={provider} />
{visibleBuckets.map((bucket, i) => (
<React.Fragment key={bucket.name}>
{i > 0 && <span className="text-muted-foreground">·</span>}
<span className="tabular-nums">
{bucket.name} {formatUsagePercentageLabel(bucket.usedPercent, display)}
</span>
</React.Fragment>
))}
{visibleBuckets.length === 0 && p.session && (
{tightestBucket ? (
<span className="tabular-nums">
{tightestBucket.name} {getDisplayedUsagePercentage(tightestBucket.usedPercent, display)}
%
</span>
) : p.session ? (
<WindowLabel
w={p.session}
label={formatWindowLabel(p.session.windowMinutes)}
display={display}
showLabel={!compact}
/>
)}
) : null}
{isStale && <AlertTriangle size={11} className="text-muted-foreground/80" />}
</span>
)
@@ -1218,29 +1244,41 @@ function ProviderSegment({
: null
].filter((w): w is { key: string; window: RateLimitWindow; label: string } => w !== null)
// Roster: show only the tightest window (highest % used) with a threshold-colored
// number so the footer stays compact; the dropdown still lists every window.
const tightestWindow =
visibleWindows.length > 0
? visibleWindows.reduce((a, b) => (b.window.usedPercent > a.window.usedPercent ? b : a))
: null
return (
<span className="inline-flex items-center gap-1.5">
<ProviderIcon provider={provider} />
{p.session && !compact && <MiniBar usedPct={clampUsedPercent(p.session.usedPercent)} />}
{visibleWindows.map((window, index) => (
<React.Fragment key={window.key}>
{index > 0 && <span className="text-muted-foreground">·</span>}
<WindowLabel w={window.window} label={window.label} display={display} />
</React.Fragment>
))}
{tightestWindow && (
<WindowLabel
w={tightestWindow.window}
label={tightestWindow.label}
display={display}
showLabel={!compact}
/>
)}
{isStale && <AlertTriangle size={11} className="text-muted-foreground/80" />}
</span>
)
}
function CodexSwitcherMenu({
export function CodexSwitcherMenu({
codex,
compact,
iconOnly
iconOnly,
asSubmenu = false,
triggerContent
}: {
codex: ProviderRateLimits
compact: boolean
iconOnly: boolean
asSubmenu?: boolean
triggerContent?: React.ReactNode
}): React.JSX.Element {
const [open, setOpen] = useState(false)
const [accountsExpanded, setAccountsExpanded] = useState(false)
@@ -1516,6 +1554,8 @@ function CodexSwitcherMenu({
provider={codex}
compact={compact}
iconOnly={iconOnly}
asSubmenu={asSubmenu}
triggerContent={triggerContent}
// Why: Codex reset credits render beside the reset action below; showing
// them in the generic provider summary duplicates the same metadata.
hidePanelResetCredits
@@ -1746,7 +1786,9 @@ export function ProviderDetailsMenu({
hidePanelResetCredits = false,
open,
onOpenChange,
children
children,
asSubmenu = false,
triggerContent
}: {
provider: ProviderRateLimits
compact: boolean
@@ -1757,6 +1799,10 @@ export function ProviderDetailsMenu({
open?: boolean
onOpenChange?: (open: boolean) => void
children?: React.ReactNode
// When set, render as a drill-in submenu (used by the consolidated Usage
// popover) with triggerContent as the full-width row instead of a segment.
asSubmenu?: boolean
triggerContent?: React.ReactNode
}): React.JSX.Element {
const recordFeatureInteraction = useAppStore((s) => s.recordFeatureInteraction)
const usagePercentageDisplay = normalizeUsagePercentageDisplay(
@@ -1772,6 +1818,41 @@ export function ProviderDetailsMenu({
onOpenChange?.(nextOpen)
}
const panelBody = (
<>
{topContent}
<div className="p-2">
<ProviderPanel
p={provider}
showResetCredits={!hidePanelResetCredits}
usagePercentageDisplay={usagePercentageDisplay}
/>
</div>
{children ? (
<>
<DropdownMenuSeparator />
{children}
</>
) : null}
</>
)
if (asSubmenu) {
return (
<DropdownMenuSub open={open} onOpenChange={handleOpenChange}>
<DropdownMenuSubTrigger className="w-full items-center gap-3 px-3.5 py-2.5">
{triggerContent}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
collisionPadding={{ top: 8, bottom: 32, left: 8, right: 8 }}
className="max-h-(--radix-dropdown-menu-content-available-height) w-[300px] overflow-y-auto p-0 scrollbar-sleek"
>
{panelBody}
</DropdownMenuSubContent>
</DropdownMenuSub>
)
}
return (
<DropdownMenu open={open} onOpenChange={handleOpenChange} modal={false}>
<DropdownMenuTrigger asChild>
@@ -1785,23 +1866,7 @@ export function ProviderDetailsMenu({
<span
className={`inline-block h-2 w-2 rounded-full ${provider.session || provider.weekly || provider.fableWeekly ? 'bg-muted-foreground/60' : 'bg-muted-foreground/30'}`}
/>
<span className="text-muted-foreground">
{provider.provider === 'claude'
? 'C'
: provider.provider === 'gemini'
? 'G'
: provider.provider === 'opencode-go'
? 'O'
: provider.provider === 'kimi'
? 'K'
: provider.provider === 'antigravity'
? 'A'
: provider.provider === 'minimax'
? 'M'
: provider.provider === 'grok'
? 'R'
: 'X'}
</span>
<span className="text-muted-foreground">{getProviderLetter(provider.provider)}</span>
</span>
) : (
<ProviderSegment p={provider} compact={compact} display={usagePercentageDisplay} />
@@ -1857,6 +1922,12 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
const rateLimits = useAppStore((s) => s.rateLimits)
const settings = useAppStore((s) => s.settings)
const refreshRateLimits = useAppStore((s) => s.refreshRateLimits)
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
const usagePercentageDisplay = normalizeUsagePercentageDisplay(
useAppStore((s) => s.usagePercentageDisplay)
)
const [usageMenuOpen, setUsageMenuOpen] = useState(false)
const statusBarVisible = useAppStore((s) => s.statusBarVisible)
const statusBarItems = useAppStore((s) => s.statusBarItems)
const recordFeatureInteraction = useAppStore((s) => s.recordFeatureInteraction)
@@ -2059,6 +2130,35 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
// floating-workspace activity and clears via the shared unread paths.
const showFloatingWorkspaceAttentionDot = !floatingTerminalOpen && hasFloatingUnread
// Consolidated roster: the leading pill dot reflects the worst usage tier across
// every visible meter.
const rosterProviders = [
showClaude ? visibleClaude : null,
showCodex ? visibleCodex : null,
showGemini ? visibleGemini : null,
showAntigravity ? visibleAntigravity : null,
showOpencodeGo ? visibleOpencodeGo : null,
showKimi ? visibleKimi : null,
showMiniMax ? visibleMiniMax : null,
showGrok ? visibleGrok : null
].filter((p): p is ProviderRateLimits => p !== null)
const handleManageAccounts = (): void => {
setUsageMenuOpen(false)
openSettingsTarget({ pane: 'accounts', repoId: null })
openSettingsPage()
}
const handleUsageDetails = (): void => {
setUsageMenuOpen(false)
openSettingsTarget({ pane: 'stats', repoId: null })
openSettingsPage()
}
const handleOpenProviderAccounts = (provider: ProviderRateLimits['provider']): void => {
setUsageMenuOpen(false)
openSettingsTarget({ pane: 'accounts', repoId: null, sectionId: `accounts-${provider}` })
openSettingsPage()
}
return (
<div
ref={containerRefCallback}
@@ -2086,81 +2186,107 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
<StatusBarUsageEmptyCta />
) : null
) : (
// Why: one-time usage-display change callout anchors to this cluster so
// it sits next to the meters the user is confused by, not a global toast.
// Consolidated roster pill → opens the all-agents Usage popover (mock parity).
<UsagePercentageDisplayChangeNotice hasVisibleUsageMeters={hasVisibleUsageMeters}>
{showClaude && (
<ClaudeSwitcherMenu claude={visibleClaude} compact={compact} iconOnly={iconOnly} />
)}
{showCodex && (
<CodexSwitcherMenu codex={visibleCodex} compact={compact} iconOnly={iconOnly} />
)}
{showGemini && (
<ProviderDetailsMenu
provider={visibleGemini}
compact={compact}
iconOnly={iconOnly}
ariaLabel={translate(
'auto.components.status.bar.StatusBar.d2375976eb',
'Open Gemini usage details'
)}
/>
)}
{showAntigravity && (
<ProviderDetailsMenu
provider={visibleAntigravity}
compact={compact}
iconOnly={iconOnly}
ariaLabel={translate(
'auto.components.status.bar.StatusBar.antigravityUsageDetails',
'Open Antigravity usage details'
)}
/>
)}
{showOpencodeGo && (
<ProviderDetailsMenu
provider={visibleOpencodeGo}
compact={compact}
iconOnly={iconOnly}
ariaLabel={translate(
'auto.components.status.bar.StatusBar.629251f4b6',
'Open OpenCode Go usage details'
)}
/>
)}
{showKimi && (
<ProviderDetailsMenu
provider={visibleKimi}
compact={compact}
iconOnly={iconOnly}
ariaLabel={translate(
'auto.components.status.bar.StatusBar.fda8146810',
'Open Kimi usage details'
)}
/>
)}
{showMiniMax && (
<ProviderDetailsMenu
provider={visibleMiniMax}
compact={compact}
iconOnly={iconOnly}
ariaLabel={translate(
'auto.components.status.bar.StatusBar.06741a2f3d',
'Open MiniMax usage details'
)}
/>
)}
{showGrok && (
<ProviderDetailsMenu
provider={visibleGrok}
compact={compact}
iconOnly={iconOnly}
ariaLabel={translate(
'auto.components.status.bar.StatusBar.grokUsageAria',
'Open Grok usage details'
)}
/>
)}
<DropdownMenu open={usageMenuOpen} onOpenChange={setUsageMenuOpen} modal={false}>
<DropdownMenuTrigger asChild>
<button
type="button"
className="inline-flex items-center gap-3 rounded px-1 py-0.5 hover:bg-accent/70"
aria-label={translate(
'auto.components.status.bar.UsageRosterPanel.title',
'Usage'
)}
>
{rosterProviders.map((p) =>
iconOnly ? (
// Narrow status bar: fall back to main's compact letter badge.
<span
key={p.provider}
className="inline-flex items-center gap-1 text-muted-foreground"
title={getProviderDisplayName(p.provider)}
>
<span
className={`inline-block h-2 w-2 rounded-full ${
p.session || p.weekly || p.fableWeekly || p.monthly || p.buckets?.length
? 'bg-muted-foreground/60'
: 'bg-muted-foreground/30'
}`}
/>
{getProviderLetter(p.provider)}
</span>
) : (
<ProviderSegment
key={p.provider}
p={p}
compact={compact}
display={usagePercentageDisplay}
/>
)
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
sideOffset={8}
// Keep the popover (and its drill-in submenus) above the status
// bar instead of overlapping it — bottom padding ≈ footer height.
collisionPadding={{ top: 8, bottom: 32, left: 8, right: 8 }}
className="w-[360px] p-0"
>
<UsageRosterPanel
providers={rosterProviders}
display={usagePercentageDisplay}
isRefreshing={isRefreshing || anyFetching}
onRefresh={handleRefresh}
onOpenProvider={handleOpenProviderAccounts}
onSignIn={handleOpenProviderAccounts}
onManageAccounts={handleManageAccounts}
onUsageDetails={handleUsageDetails}
renderRow={(p, rowNode) => {
// Every provider drills into its detail panel (parity with the
// per-provider dropdowns on main); Claude/Codex additionally get
// the account switcher + runtime toggle + Codex reset credits.
if (p.provider === 'claude') {
return (
<ClaudeSwitcherMenu
claude={p}
compact={compact}
iconOnly={false}
asSubmenu
triggerContent={rowNode}
/>
)
}
if (p.provider === 'codex') {
return (
<CodexSwitcherMenu
codex={p}
compact={compact}
iconOnly={false}
asSubmenu
triggerContent={rowNode}
/>
)
}
return (
<ProviderDetailsMenu
provider={p}
compact={compact}
iconOnly={false}
asSubmenu
triggerContent={rowNode}
ariaLabel={translate(
'auto.components.status.bar.UsageRosterPanel.openDetails',
'Open usage details'
)}
/>
)
}}
/>
</DropdownMenuContent>
</DropdownMenu>
</UsagePercentageDisplayChangeNotice>
)}
{anyVisible && !isEmptyUsageState && (
@@ -0,0 +1,232 @@
import React from 'react'
import { ChevronRight, RefreshCw } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { formatWindowLabel } from '@/lib/window-label-formatter'
import type { ProviderRateLimits, RateLimitWindow } from '../../../../shared/rate-limit-types'
import {
clampUsedPercent,
getDisplayedUsagePercentage,
type UsagePercentageDisplay
} from '../../../../shared/usage-percentage-display'
import { barColor, formatResetCountdown, getWindowSections, ProviderIcon } from './tooltip'
import { getProviderDisplayName } from './usage-error-copy'
import { formatPlanLabel, usageTextColorClass } from './usage-roster-formatting'
type ProviderId = ProviderRateLimits['provider']
type Section = { label: string; window: RateLimitWindow }
// Windows/buckets that actually carry data — the null ones are absent limits.
function usedSections(p: ProviderRateLimits): Section[] {
return getWindowSections(p).filter((s): s is Section => s.window !== null)
}
function providerMaxUsed(sections: Section[]): number {
return sections.length > 0
? Math.max(...sections.map((s) => clampUsedPercent(s.window.usedPercent)))
: 0
}
// Buckets (Gemini Flash/Pro) keep their model name; windows use their duration.
function shortLabel(p: ProviderRateLimits, section: Section): string {
if (p.buckets?.some((b) => b.name === section.label)) {
return section.label
}
// fableWeekly shares the 7d window with weekly; label it distinctly so the two
// don't both render as "wk".
if (section.window === p.fableWeekly) {
return 'Fable'
}
return formatWindowLabel(section.window.windowMinutes)
}
// The soonest-resetting window summarizes the agent's next reset in one line.
function soonestResetLabel(sections: Section[]): string | null {
const resets = sections
.map((s) => s.window.resetsAt)
.filter((r): r is number => typeof r === 'number')
if (resets.length === 0) {
return null
}
return formatResetCountdown(Math.min(...resets) - Date.now())
}
// Presentational row: a compact header (icon · name · plan · reset) with
// the per-window metrics beneath, so the reset stays visible and multi-window
// agents stay short. The wrapper supplies padding + interaction (drill-in
// submenu or plain clickable row).
export function UsageRow({
p,
display,
onSignIn
}: {
p: ProviderRateLimits
display: UsagePercentageDisplay
onSignIn: (provider: ProviderId) => void
}): React.JSX.Element {
const sections = usedSections(p)
const signedOut = sections.length === 0
const name = getProviderDisplayName(p.provider)
const plan = formatPlanLabel(p.planType)
const reset = signedOut ? null : soonestResetLabel(sections)
return (
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex items-center gap-2.5">
<span className="flex size-5 shrink-0 items-center justify-center rounded-md border border-border bg-secondary">
<ProviderIcon provider={p.provider} />
</span>
<span className="min-w-0 shrink truncate text-[13px] font-medium text-foreground">
{name}
{plan ? <span className="font-normal text-muted-foreground"> · {plan}</span> : null}
</span>
{!signedOut && reset ? (
<span className="shrink-0 text-[11px] text-muted-foreground">{reset}</span>
) : null}
{signedOut ? (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onSignIn(p.provider)
}}
className="ml-auto shrink-0 rounded-md border border-border bg-secondary px-2.5 py-0.5 text-xs text-foreground hover:bg-accent"
>
{translate('auto.components.status.bar.StatusBar.c35af53b73', 'Sign in')}
</button>
) : null}
</div>
{signedOut ? (
<div className="pl-[30px] text-[11px] text-muted-foreground">
{translate('auto.components.status.bar.UsageRosterPanel.notSignedIn', 'not signed in')}
</div>
) : (
<div className="flex flex-wrap items-center gap-x-2.5 gap-y-1 pl-[30px]">
{sections.map((s) => {
const used = clampUsedPercent(s.window.usedPercent)
const shown = getDisplayedUsagePercentage(s.window.usedPercent, display)
return (
<span key={s.label} className="flex items-center gap-1.5">
<span className="text-[10px] text-muted-foreground">{shortLabel(p, s)}</span>
<span className="h-[5px] w-7 overflow-hidden rounded-full bg-muted">
<span
className={`block h-full rounded-full ${barColor(used)}`}
style={{ width: `${used}%` }}
/>
</span>
<span className={`tabular-nums text-[11px] ${usageTextColorClass(used)}`}>
{shown}%
</span>
</span>
)
})}
</div>
)}
</div>
)
}
/**
* Consolidated "Usage" popover — one row per agent (icon · name · reset ·
* per-window bars), opened from the status-bar roster pill. Deep per-agent
* actions route to Settings via the callbacks.
*/
export function UsageRosterPanel({
providers,
display,
isRefreshing,
onRefresh,
onOpenProvider,
onSignIn,
onManageAccounts,
onUsageDetails,
renderRow
}: {
providers: ProviderRateLimits[]
display: UsagePercentageDisplay
isRefreshing: boolean
onRefresh: () => void
onOpenProvider: (provider: ProviderId) => void
onSignIn: (provider: ProviderId) => void
onManageAccounts: () => void
onUsageDetails: () => void
// Lets the host wrap a provider's row in a richer control (e.g. the
// Claude/Codex account-switch drill-in submenu); return null to use the
// default clickable row.
renderRow?: (p: ProviderRateLimits, row: React.ReactNode) => React.ReactNode
}): React.JSX.Element {
// Worst-first so the agent nearest a limit sits on top.
const sorted = [...providers].sort(
(a, b) => providerMaxUsed(usedSections(b)) - providerMaxUsed(usedSections(a))
)
return (
<div className="w-[360px] text-xs">
<div className="flex items-center justify-between px-3.5 pb-2 pt-3">
<span className="text-[13px] font-semibold text-foreground">
{translate('auto.components.status.bar.UsageRosterPanel.title', 'Usage')}
</span>
<div className="flex items-center gap-2 text-muted-foreground">
<span className="text-[11px]">
{translate('auto.components.status.bar.UsageRosterPanel.allAgents', 'all agents')}
</span>
<button
type="button"
onClick={onRefresh}
aria-label={translate(
'auto.components.status.bar.StatusBar.3325d996cb',
'Refresh rate limits'
)}
className="rounded p-0.5 hover:bg-accent hover:text-foreground"
>
<RefreshCw size={12} className={isRefreshing ? 'animate-spin' : ''} />
</button>
</div>
</div>
<div className="border-t border-border/70" />
{sorted.map((p) => {
const rowNode = <UsageRow p={p} display={display} onSignIn={onSignIn} />
const custom = renderRow?.(p, rowNode)
if (custom) {
return <React.Fragment key={p.provider}>{custom}</React.Fragment>
}
return (
<div
key={p.provider}
role="button"
tabIndex={0}
onClick={() => onOpenProvider(p.provider)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onOpenProvider(p.provider)
}
}}
className="flex cursor-pointer items-center px-3.5 py-2.5 hover:bg-accent/60"
>
{rowNode}
</div>
)
})}
<div className="border-t border-border/70" />
<button
type="button"
onClick={onUsageDetails}
className="flex w-full items-center justify-between px-3.5 py-2.5 text-[13px] text-foreground hover:bg-accent/60"
>
{translate(
'auto.components.status.bar.UsageRosterPanel.usageDetails',
'Usage details & history'
)}
<ChevronRight size={14} className="text-muted-foreground" />
</button>
<button
type="button"
onClick={onManageAccounts}
className="flex w-full items-center justify-between px-3.5 py-2.5 text-[13px] text-foreground hover:bg-accent/60"
>
{translate('auto.components.status.bar.StatusBar.75ded02687', 'Manage Accounts…')}
<ChevronRight size={14} className="text-muted-foreground" />
</button>
</div>
)
}
@@ -518,11 +518,12 @@ describe('clampUsedPercent', () => {
})
describe('barColor', () => {
// Why: thresholds are on % used (consumption). Guard against flipping back
// to remaining-based colors without noticing.
it('maps used percent to green / yellow / red bands', () => {
expect(barColor(0)).toBe('bg-green-500')
expect(barColor(59)).toBe('bg-green-500')
// Why: thresholds are on % used (consumption). The <60 band is neutral (not
// green) so the always-visible meter stays quiet until a limit nears; guard
// against flipping back to green or to remaining-based colors without noticing.
it('maps used percent to neutral / yellow / red bands', () => {
expect(barColor(0)).toBe('bg-muted-foreground/40')
expect(barColor(59)).toBe('bg-muted-foreground/40')
expect(barColor(60)).toBe('bg-yellow-500')
expect(barColor(79)).toBe('bg-yellow-500')
expect(barColor(80)).toBe('bg-red-500')
@@ -201,10 +201,12 @@ export function getWindowSections(
// Why: color-coded by consumption so users can quickly gauge urgency.
// Matches common harness usage meters (Claude/Codex): bars fill with % used.
// Green = comfortable (<60% used), yellow = caution (60-80%), red = critical (≥80%).
// Neutral = comfortable (<60% used), yellow = caution (60-80%), red = critical (≥80%).
// Why: the <60 band is neutral (not green) so the always-visible meter stays quiet
// until a limit nears — color = state (STYLEGUIDE) — and matches MiniBar's grey fill.
export function barColor(usedPct: number): string {
if (usedPct < 60) {
return 'bg-green-500'
return 'bg-muted-foreground/40'
}
if (usedPct < 80) {
return 'bg-yellow-500'
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest'
import { formatPlanLabel, usageTextColorClass } from './usage-roster-formatting'
describe('formatPlanLabel', () => {
it('capitalizes a single-word plan', () => {
expect(formatPlanLabel('plus')).toBe('Plus')
expect(formatPlanLabel('pro')).toBe('Pro')
expect(formatPlanLabel('business')).toBe('Business')
})
it('title-cases multi-token plans across separators', () => {
expect(formatPlanLabel('chatgpt_business')).toBe('Chatgpt Business')
expect(formatPlanLabel('team-plus')).toBe('Team Plus')
expect(formatPlanLabel('pro trial')).toBe('Pro Trial')
})
it('returns null when there is no usable plan', () => {
expect(formatPlanLabel(null)).toBeNull()
expect(formatPlanLabel(undefined)).toBeNull()
expect(formatPlanLabel('')).toBeNull()
expect(formatPlanLabel(' ')).toBeNull()
})
})
describe('usageTextColorClass', () => {
it('stays neutral below the 60% caution line', () => {
expect(usageTextColorClass(0)).toBe('text-foreground')
expect(usageTextColorClass(59)).toBe('text-foreground')
})
it('turns amber in the 60–79% caution band', () => {
expect(usageTextColorClass(60)).toBe('text-yellow-500')
expect(usageTextColorClass(79)).toBe('text-yellow-500')
})
it('turns red at the 80% critical line and above', () => {
expect(usageTextColorClass(80)).toBe('text-red-500')
expect(usageTextColorClass(100)).toBe('text-red-500')
})
})
@@ -0,0 +1,27 @@
// Pure formatting for the consolidated Usage roster, split out so it can be unit
// tested without pulling in React / UI dependencies.
// "plus" -> "Plus", "chatgpt_business" -> "Chatgpt Business". Codex is the only
// provider that reports a plan today; others render just the name.
export function formatPlanLabel(planType: string | null | undefined): string | null {
const trimmed = planType?.trim()
if (!trimmed) {
return null
}
return trimmed
.split(/[\s_-]+/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
// Mirrors barColor's 60/80 thresholds so the number matches its bar; neutral
// inherits the foreground color (STYLEGUIDE: color reserved for state).
export function usageTextColorClass(usedPercent: number): string {
if (usedPercent >= 80) {
return 'text-red-500'
}
if (usedPercent >= 60) {
return 'text-yellow-500'
}
return 'text-foreground'
}
+7
View File
@@ -3230,6 +3230,13 @@
"dismiss": "Dismiss",
"openSettings": "Open Settings",
"gotIt": "Got it"
},
"UsageRosterPanel": {
"title": "Usage",
"openDetails": "Open usage details",
"notSignedIn": "not signed in",
"allAgents": "all agents",
"usageDetails": "Usage details & history"
}
}
},
+7
View File
@@ -3230,6 +3230,13 @@
"dismiss": "Dismiss",
"openSettings": "Open Settings",
"gotIt": "Got it"
},
"UsageRosterPanel": {
"title": "Usage",
"openDetails": "Open usage details",
"notSignedIn": "not signed in",
"allAgents": "all agents",
"usageDetails": "Usage details & history"
}
}
},
+7
View File
@@ -3230,6 +3230,13 @@
"dismiss": "Dismiss",
"openSettings": "Open Settings",
"gotIt": "Got it"
},
"UsageRosterPanel": {
"title": "Usage",
"openDetails": "Open usage details",
"notSignedIn": "not signed in",
"allAgents": "all agents",
"usageDetails": "Usage details & history"
}
}
},
+7
View File
@@ -3230,6 +3230,13 @@
"dismiss": "Dismiss",
"openSettings": "Open Settings",
"gotIt": "Got it"
},
"UsageRosterPanel": {
"title": "Usage",
"openDetails": "Open usage details",
"notSignedIn": "not signed in",
"allAgents": "all agents",
"usageDetails": "Usage details & history"
}
}
},
+7
View File
@@ -3230,6 +3230,13 @@
"dismiss": "Dismiss",
"openSettings": "Open Settings",
"gotIt": "Got it"
},
"UsageRosterPanel": {
"title": "Usage",
"openDetails": "Open usage details",
"notSignedIn": "not signed in",
"allAgents": "all agents",
"usageDetails": "Usage details & history"
}
}
},
+2
View File
@@ -76,6 +76,8 @@ export type ProviderRateLimits = {
grantedAt: number | null
}[]
} | null
/** Subscription plan tier for the active account (Codex `plan_type`, e.g. "plus"). */
planType?: string | null
/** Unix ms timestamp of the last successful data update. */
updatedAt: number
/** Human-readable error message, null when status is 'ok'. */