From 97cd05b02e2a0482646558569703cb1429e611ce Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:22:50 -0700 Subject: [PATCH] feat(status-bar): consolidate agent usage into a single roster popover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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". --- src/main/rate-limits/codex-fetcher.ts | 2 + .../src/components/status-bar/StatusBar.tsx | 386 ++++++++++++------ .../status-bar/UsageRosterPanel.tsx | 232 +++++++++++ .../src/components/status-bar/tooltip.test.ts | 11 +- .../src/components/status-bar/tooltip.tsx | 6 +- .../usage-roster-formatting.test.ts | 40 ++ .../status-bar/usage-roster-formatting.ts | 27 ++ src/renderer/src/i18n/locales/en.json | 7 + src/renderer/src/i18n/locales/es.json | 7 + src/renderer/src/i18n/locales/ja.json | 7 + src/renderer/src/i18n/locales/ko.json | 7 + src/renderer/src/i18n/locales/zh.json | 7 + src/shared/rate-limit-types.ts | 2 + 13 files changed, 604 insertions(+), 137 deletions(-) create mode 100644 src/renderer/src/components/status-bar/UsageRosterPanel.tsx create mode 100644 src/renderer/src/components/status-bar/usage-roster-formatting.test.ts create mode 100644 src/renderer/src/components/status-bar/usage-roster-formatting.ts diff --git a/src/main/rate-limits/codex-fetcher.ts b/src/main/rate-limits/codex-fetcher.ts index d606a12e850..3e151c50284 100644 --- a/src/main/rate-limits/codex-fetcher.ts +++ b/src/main/rate-limits/codex-fetcher.ts @@ -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: diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx index b3cc8a922c1..e93bd9e6bbc 100644 --- a/src/renderer/src/components/status-bar/StatusBar.tsx +++ b/src/renderer/src/components/status-bar/StatusBar.tsx @@ -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({ ) } -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 ( -
-
-
- ) -} - // --------------------------------------------------------------------------- // 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 ( - {formatUsagePercentageLabel(w.usedPercent, display)} {label} + {getDisplayedUsagePercentage(w.usedPercent, display)}%{showLabel ? ` ${label}` : ''} ) } +// 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 ( - {visibleBuckets.map((bucket, i) => ( - - {i > 0 && ·} - - {bucket.name} {formatUsagePercentageLabel(bucket.usedPercent, display)} - - - ))} - {visibleBuckets.length === 0 && p.session && ( + {tightestBucket ? ( + + {tightestBucket.name} {getDisplayedUsagePercentage(tightestBucket.usedPercent, display)} + % + + ) : p.session ? ( - )} + ) : null} {isStale && } ) @@ -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 ( - {p.session && !compact && } - {visibleWindows.map((window, index) => ( - - {index > 0 && ·} - - - ))} + {tightestWindow && ( + + )} {isStale && } ) } -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} +
+ +
+ {children ? ( + <> + + {children} + + ) : null} + + ) + + if (asSubmenu) { + return ( + + + {triggerContent} + + + {panelBody} + + + ) + } + return ( @@ -1785,23 +1866,7 @@ export function ProviderDetailsMenu({ - - {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'} - + {getProviderLetter(provider.provider)} ) : ( @@ -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 (
) : 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). - {showClaude && ( - - )} - {showCodex && ( - - )} - {showGemini && ( - - )} - {showAntigravity && ( - - )} - {showOpencodeGo && ( - - )} - {showKimi && ( - - )} - {showMiniMax && ( - - )} - {showGrok && ( - - )} + + + + + + { + // 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 ( + + ) + } + if (p.provider === 'codex') { + return ( + + ) + } + return ( + + ) + }} + /> + + )} {anyVisible && !isEmptyUsageState && ( diff --git a/src/renderer/src/components/status-bar/UsageRosterPanel.tsx b/src/renderer/src/components/status-bar/UsageRosterPanel.tsx new file mode 100644 index 00000000000..f1087785b78 --- /dev/null +++ b/src/renderer/src/components/status-bar/UsageRosterPanel.tsx @@ -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 ( +
+
+ + + + + {name} + {plan ? · {plan} : null} + + {!signedOut && reset ? ( + {reset} + ) : null} + {signedOut ? ( + + ) : null} +
+ {signedOut ? ( +
+ {translate('auto.components.status.bar.UsageRosterPanel.notSignedIn', 'not signed in')} +
+ ) : ( +
+ {sections.map((s) => { + const used = clampUsedPercent(s.window.usedPercent) + const shown = getDisplayedUsagePercentage(s.window.usedPercent, display) + return ( + + {shortLabel(p, s)} + + + + + {shown}% + + + ) + })} +
+ )} +
+ ) +} + +/** + * 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 ( +
+
+ + {translate('auto.components.status.bar.UsageRosterPanel.title', 'Usage')} + +
+ + {translate('auto.components.status.bar.UsageRosterPanel.allAgents', 'all agents')} + + +
+
+
+ {sorted.map((p) => { + const rowNode = + const custom = renderRow?.(p, rowNode) + if (custom) { + return {custom} + } + return ( +
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} +
+ ) + })} +
+ + +
+ ) +} diff --git a/src/renderer/src/components/status-bar/tooltip.test.ts b/src/renderer/src/components/status-bar/tooltip.test.ts index c553f68ddb1..59540c432eb 100644 --- a/src/renderer/src/components/status-bar/tooltip.test.ts +++ b/src/renderer/src/components/status-bar/tooltip.test.ts @@ -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') diff --git a/src/renderer/src/components/status-bar/tooltip.tsx b/src/renderer/src/components/status-bar/tooltip.tsx index c2fde5de91a..18210c5a37f 100644 --- a/src/renderer/src/components/status-bar/tooltip.tsx +++ b/src/renderer/src/components/status-bar/tooltip.tsx @@ -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' diff --git a/src/renderer/src/components/status-bar/usage-roster-formatting.test.ts b/src/renderer/src/components/status-bar/usage-roster-formatting.test.ts new file mode 100644 index 00000000000..fad599cc995 --- /dev/null +++ b/src/renderer/src/components/status-bar/usage-roster-formatting.test.ts @@ -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') + }) +}) diff --git a/src/renderer/src/components/status-bar/usage-roster-formatting.ts b/src/renderer/src/components/status-bar/usage-roster-formatting.ts new file mode 100644 index 00000000000..499b026680f --- /dev/null +++ b/src/renderer/src/components/status-bar/usage-roster-formatting.ts @@ -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' +} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index d6d8725c2d8..da6543aaa77 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -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" } } }, diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index a2bbc411665..3e7903f2d72 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -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" } } }, diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 8ce317e4ae4..ba98aa6cb3a 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -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" } } }, diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 37326de084a..ceb5123182e 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -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" } } }, diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 060903dce0e..d06e079c1d7 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -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" } } }, diff --git a/src/shared/rate-limit-types.ts b/src/shared/rate-limit-types.ts index 65c09aaffae..5ec9a1aaf86 100644 --- a/src/shared/rate-limit-types.ts +++ b/src/shared/rate-limit-types.ts @@ -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'. */