diff --git a/mobile/src/components/AccountUsage.tsx b/mobile/src/components/AccountUsage.tsx index 9d85bf434..00b3e6074 100644 --- a/mobile/src/components/AccountUsage.tsx +++ b/mobile/src/components/AccountUsage.tsx @@ -21,9 +21,9 @@ export { hasRenderableUsage } from './account-usage-state' -// Why: matches desktop StatusBar convention — bars show percent remaining -// (so a fresh account renders full, a depleted one renders empty), not -// percent used. Color thresholds invert accordingly. +// Why: matches desktop StatusBar — bars show percent used (consumption), same +// as Claude/Codex harness meters. Fresh account is empty/green; depleted is +// full/red. export function UsageBar({ label, usedPercent, @@ -35,13 +35,15 @@ export function UsageBar({ unavailable: boolean loading?: boolean }) { - const remaining = usedPercent == null ? null : Math.max(0, Math.min(100, 100 - usedPercent)) + // Why: round then clamp so bar width, color, and label share one value (desktop parity). + const used = usedPercent == null ? null : Math.max(0, Math.min(100, Math.round(usedPercent))) + // Why: same consumption bands as desktop barColor (green <60, amber <80, red ≥80). const barColor = - remaining == null + used == null ? colors.textMuted - : remaining <= 10 + : used >= 80 ? colors.statusRed - : remaining <= 30 + : used >= 60 ? colors.statusAmber : colors.statusGreen return ( @@ -52,7 +54,7 @@ export function UsageBar({ style={[ styles.usageFill, { - width: `${remaining ?? 0}%`, + width: `${used ?? 0}%`, backgroundColor: unavailable ? colors.textMuted : barColor } ]} @@ -61,9 +63,7 @@ export function UsageBar({ {loading ? ( ) : ( - - {unavailable || remaining == null ? '—' : `${Math.round(remaining)}%`} - + {unavailable || used == null ? '—' : `${used}%`} )} ) diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index a85d7fb6c..56841b772 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -2433,7 +2433,7 @@ } /* Brief one-shot pulse when the Codex limits popover swaps accounts and the - "100% left" label flashes from red to green. */ + "0% used" label flashes from red to green. */ @keyframes feature-wall-meta-pulse { 0% { color: rgb(220 38 38); diff --git a/src/renderer/src/components/feature-wall/agents-orchestration/UsagePage.tsx b/src/renderer/src/components/feature-wall/agents-orchestration/UsagePage.tsx index faac3d456..a7f73f3b9 100644 --- a/src/renderer/src/components/feature-wall/agents-orchestration/UsagePage.tsx +++ b/src/renderer/src/components/feature-wall/agents-orchestration/UsagePage.tsx @@ -77,10 +77,11 @@ function Popover(props: { pulseKey: number }): JSX.Element { const { expanded, targeted, swapped, pulseKey } = props - const sessionPctText = swapped ? '100% left' : '4% left' + // Why: bars show % used (consumption), matching the live status-bar meter. + const sessionPctText = swapped ? '0% used' : '96% used' const sessionResetText = swapped ? 'Resets in 5h' : 'Resets in 47m' - const sessionFillWidth = swapped ? '100%' : '4%' - const weeklyFillWidth = '62%' + const sessionFillWidth = swapped ? '0%' : '96%' + const weeklyFillWidth = '38%' return (
{translate( 'auto.components.feature.wall.agents.orchestration.UsagePage.05ce4ecdd3', - '62% left' + '38% used' )} } @@ -201,15 +202,15 @@ function Popover(props: {
@@ -295,11 +296,12 @@ function SwitchAccount(props: { // The 340px-wide light pill at the bottom of the panel that the popover tip // points down to. Codex chip is the "active" one — it animates from a near- -// empty red bar to a full green bar after the account swap. +// full red bar (% used) to an empty green bar after the account swap. function BottomBar(props: { swapped: boolean }): JSX.Element { - const codexFillWidth = props.swapped ? '100%' : '4%' + // Why: match live status-bar consumption meters (% used), same as the popover. + const codexFillWidth = props.swapped ? '0%' : '96%' const codexFillColor = props.swapped ? 'rgb(34 197 94)' : 'rgb(239 68 68)' - const codexMeta = props.swapped ? '100% 5h · 96% wk' : '4% 47m' + const codexMeta = props.swapped ? '0% used 5h · 4% used wk' : '96% used 47m' return (
- + {translate( 'auto.components.feature.wall.agents.orchestration.UsagePage.64265cb295', - '71% 5h' + '29% used 5h' )}
diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx index 71cc71ffe..59c9fe08e 100644 --- a/src/renderer/src/components/status-bar/StatusBar.tsx +++ b/src/renderer/src/components/status-bar/StatusBar.tsx @@ -51,6 +51,7 @@ import { ProviderIcon, ProviderPanel, barColor, + clampUsedPercent, formatResetCreditExpiry, getProviderUsageStatusLabel } from './tooltip' @@ -932,15 +933,15 @@ function ClaudeSwitcherMenu({ } // --------------------------------------------------------------------------- -// Mini progress bar (shows remaining capacity, grey) +// Mini progress bar (shows consumption / % used, grey) // --------------------------------------------------------------------------- -function MiniBar({ leftPct }: { leftPct: number }): React.JSX.Element { +function MiniBar({ usedPct }: { usedPct: number }): React.JSX.Element { return (
) @@ -957,29 +958,32 @@ export function InlineUsageBars({ limits: ProviderRateLimits isFetching: boolean }): React.JSX.Element { + // Why: show % used (consumption), not remaining — matches harness meters (#7551). + // Keep the "used" word in compact labels so bare "32% 5h" is not read as remaining. + const usedSuffix = translate('auto.components.status.bar.tooltip.cedb7b99e3', '% used') const usageWindows = [ limits.session ? { key: 'session', - left: Math.max(0, Math.round(100 - limits.session.usedPercent)), - label: translate('auto.components.status.bar.StatusBar.d79c3362c4', '% 5h') + used: clampUsedPercent(limits.session.usedPercent), + label: translate('auto.components.status.bar.StatusBar.d79c3362c4', '5h') } : null, limits.weekly ? { key: 'weekly', - left: Math.max(0, Math.round(100 - limits.weekly.usedPercent)), - label: translate('auto.components.status.bar.StatusBar.5c938d39ac', '% wk') + used: clampUsedPercent(limits.weekly.usedPercent), + label: translate('auto.components.status.bar.StatusBar.5c938d39ac', 'wk') } : null, limits.fableWeekly ? { key: 'fableWeekly', - left: Math.max(0, Math.round(100 - limits.fableWeekly.usedPercent)), - label: translate('auto.components.status.bar.StatusBar.54e8d6bb2d', '% Fable') + used: clampUsedPercent(limits.fableWeekly.usedPercent), + label: translate('auto.components.status.bar.StatusBar.54e8d6bb2d', 'Fable') } : null - ].filter((window): window is { key: string; left: number; label: string } => window !== null) + ].filter((window): window is { key: string; used: number; label: string } => window !== null) return (
- {window.left} - {window.label} + {window.used} + {usedSuffix} {window.label}
))} @@ -1071,14 +1075,16 @@ function InlineUsageSkeleton(): React.JSX.Element { } // --------------------------------------------------------------------------- -// Window label (shows percent remaining) +// Window label (shows percent used / consumption) // --------------------------------------------------------------------------- function WindowLabel({ w, label }: { w: RateLimitWindow; label: string }): React.JSX.Element { - const left = Math.max(0, Math.round(100 - w.usedPercent)) + const used = clampUsedPercent(w.usedPercent) + // Why: "32% 5h" is ambiguous after the remaining→used flip; keep "used" explicit. return ( - {left}% {label} + {used} + {translate('auto.components.status.bar.tooltip.cedb7b99e3', '% used')} {label} ) } @@ -1150,12 +1156,13 @@ function ProviderSegment({ {visibleBuckets.map((bucket, i) => { - const left = Math.max(0, Math.round(100 - bucket.usedPercent)) + const used = clampUsedPercent(bucket.usedPercent) return ( {i > 0 && ·} - {bucket.name} {left}% + {bucket.name} {used} + {translate('auto.components.status.bar.tooltip.cedb7b99e3', '% used')} ) @@ -1195,7 +1202,7 @@ function ProviderSegment({ return ( - {p.session && !compact && } + {p.session && !compact && } {visibleWindows.map((window, index) => ( {index > 0 && ·} diff --git a/src/renderer/src/components/status-bar/inline-usage-bars.test.tsx b/src/renderer/src/components/status-bar/inline-usage-bars.test.tsx index 0160978be..018d26076 100644 --- a/src/renderer/src/components/status-bar/inline-usage-bars.test.tsx +++ b/src/renderer/src/components/status-bar/inline-usage-bars.test.tsx @@ -46,8 +46,9 @@ describe('InlineUsageBars', () => { ) - expect(markup).toContain('68% 5h') - expect(markup).toContain('84% wk') - expect(markup).toContain('58% Fable') + // Why: bars show % used with explicit "used" so compact labels are not ambiguous. + expect(markup).toContain('32% used 5h') + expect(markup).toContain('16% used wk') + expect(markup).toContain('42% used Fable') }) }) diff --git a/src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx b/src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx index 143c562fc..25dfa651b 100644 --- a/src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx +++ b/src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx @@ -28,7 +28,8 @@ vi.mock('./tooltip', () => ({ ProviderPanel: function ProviderPanel(props: Record) { return { type: 'ProviderPanel', props } }, - barColor: () => 'bg-green-500' + barColor: () => 'bg-green-500', + clampUsedPercent: (n: number) => Math.max(0, Math.min(100, Math.round(n))) })) vi.mock('@/components/ui/dropdown-menu', () => ({ diff --git a/src/renderer/src/components/status-bar/tooltip.test.ts b/src/renderer/src/components/status-bar/tooltip.test.ts index dea9af3ab..8f214ecc7 100644 --- a/src/renderer/src/components/status-bar/tooltip.test.ts +++ b/src/renderer/src/components/status-bar/tooltip.test.ts @@ -22,6 +22,8 @@ vi.mock('@/i18n/i18n', () => ({ })) import { + barColor, + clampUsedPercent, formatResetCreditExpiry, formatResetCountdown, getProviderUsageErrorMessage, @@ -409,7 +411,7 @@ describe('ProviderPanel reset rendering', () => { expect(markup).toContain('Resets in 6d 17h') }) - it('renders MiniMax session as `100 - usedPercent` left so the value matches the bar', () => { + it('renders MiniMax session as usedPercent so the value matches the bar', () => { vi.useFakeTimers() vi.setSystemTime(new Date(2026, 6, 4, 15, 0)) const p = provider({ @@ -425,14 +427,13 @@ describe('ProviderPanel reset rendering', () => { const markup = renderToStaticMarkup(ProviderPanel({ p })) - // Why: the bar reads "65% 5h"; the tooltip must read "65% left" from the - // same source field so the two views stay consistent. - expect(markup).toContain('65%') - expect(markup).toContain('% left') - expect(markup).not.toContain('100% left') + // Why: bars show consumption (% used), matching harness meters (#7551). + expect(markup).toContain('35%') + expect(markup).toContain('% used') + expect(markup).not.toContain('% left') }) - it('clamps MiniMax session to 0% left when usedPercent reports 100', () => { + it('clamps MiniMax session to 100% used when usedPercent reports 100', () => { vi.useFakeTimers() vi.setSystemTime(new Date(2026, 6, 4, 15, 0)) const p = provider({ @@ -448,7 +449,52 @@ describe('ProviderPanel reset rendering', () => { const markup = renderToStaticMarkup(ProviderPanel({ p })) - expect(markup).toContain('0% left') + expect(markup).toContain('100%') + expect(markup).toContain('% used') + }) + + it('clamps over-100 usedPercent to 100% used in the panel', () => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 6, 4, 15, 0)) + const p = provider({ + provider: 'minimax', + status: 'ok', + session: { + usedPercent: 140, + windowMinutes: 300, + resetsAt: Date.now() + 2 * 60 * 60_000, + resetDescription: null + } + }) + + const markup = renderToStaticMarkup(ProviderPanel({ p })) + + expect(markup).toContain('100%') + expect(markup).toContain('width:100%') + expect(markup).not.toContain('140%') + }) +}) + +describe('clampUsedPercent', () => { + it('rounds and clamps into 0–100', () => { + expect(clampUsedPercent(-3)).toBe(0) + expect(clampUsedPercent(32.4)).toBe(32) + expect(clampUsedPercent(32.6)).toBe(33) + expect(clampUsedPercent(100)).toBe(100) + expect(clampUsedPercent(140)).toBe(100) + }) +}) + +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') + expect(barColor(60)).toBe('bg-yellow-500') + expect(barColor(79)).toBe('bg-yellow-500') + expect(barColor(80)).toBe('bg-red-500') + expect(barColor(100)).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 be174965b..47ba64142 100644 --- a/src/renderer/src/components/status-bar/tooltip.tsx +++ b/src/renderer/src/components/status-bar/tooltip.tsx @@ -191,13 +191,19 @@ export function getWindowSections( // `text-background` for primary text and `text-background/50` for secondary // to stay readable inside the inverted tooltip container. -// Why: color-coded by remaining capacity so users can quickly gauge urgency. -// Green = comfortable (>40% left), yellow = caution (20-40%), red = critical (<20%). -export function barColor(leftPct: number): string { - if (leftPct > 40) { +// Why: single clamp for bar width + label so status bar and tooltip never diverge. +export function clampUsedPercent(usedPercent: number): number { + return Math.max(0, Math.min(100, Math.round(usedPercent))) +} + +// 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%). +export function barColor(usedPct: number): string { + if (usedPct < 60) { return 'bg-green-500' } - if (leftPct > 20) { + if (usedPct < 80) { return 'bg-yellow-500' } return 'bg-red-500' @@ -282,7 +288,9 @@ export function ProviderPanel({ if (!w) { return null } - const leftPct = Math.max(0, Math.round(100 - w.usedPercent)) + // Why: show % used (consumption), not remaining — matches Claude/Codex + // harness meters and avoids the "full green bar = exhausted" misread (#7551). + const usedPct = clampUsedPercent(w.usedPercent) const resetLabel = w.resetsAt ? formatResetCountdown(w.resetsAt - Date.now()) : null return ( @@ -290,14 +298,14 @@ export function ProviderPanel({
{label}
- {leftPct} - {translate('auto.components.status.bar.tooltip.cedb7b99e3', '% left')} + {usedPct} + {translate('auto.components.status.bar.tooltip.cedb7b99e3', '% used')} {resetLabel && {resetLabel}}
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 8a91f7965..c0f48f1cc 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -2999,9 +2999,9 @@ "2483c60695": "···", "c35af53b73": "Sign in", "f19a63e7cd": "Sign in to see usage", - "5c938d39ac": "% wk", - "54e8d6bb2d": "% Fable", - "d79c3362c4": "% 5h", + "5c938d39ac": "wk", + "54e8d6bb2d": "Fable", + "d79c3362c4": "5h", "a79c64f87e": "Fable", "8295903d17": "Restart live Claude terminals before continuing old conversations after switching.", "c98ea88392": "No other accounts", @@ -3169,7 +3169,7 @@ } }, "tooltip": { - "cedb7b99e3": "% left", + "cedb7b99e3": "% used", "6d6df77f41": "No data available", "7f7f208060": "Monthly", "252c096536": "Weekly", @@ -10996,11 +10996,11 @@ "945865332e": "Signing in" }, "UsagePage": { - "64265cb295": "71% 5h", + "64265cb295": "29% used 5h", "be5a165875": "Switch to", "277a9c65a9": "Codex Account", "4dce5ca3aa": "Resets in 4d 3h", - "05ce4ecdd3": "62% left", + "05ce4ecdd3": "38% used", "0470aaed99": "Weekly", "f421abf962": "Session", "5e45fb1238": "Updated 1m ago", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 694ea52f8..37f554861 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -2999,9 +2999,9 @@ "2483c60695": "···", "c35af53b73": "Iniciar sesión", "f19a63e7cd": "Inicia sesión para ver el uso", - "5c938d39ac": "% semana", - "54e8d6bb2d": "% Fable", - "d79c3362c4": "% 5h", + "5c938d39ac": "semana", + "54e8d6bb2d": "Fable", + "d79c3362c4": "5h", "a79c64f87e": "Fable", "8295903d17": "Reinicia los terminales Claude activos antes de continuar conversaciones antiguas después de cambiar.", "c98ea88392": "Ninguna otra cuenta", @@ -3169,7 +3169,7 @@ } }, "tooltip": { - "cedb7b99e3": "% restante", + "cedb7b99e3": "% usado", "6d6df77f41": "No hay datos disponibles", "7f7f208060": "Mensual", "252c096536": "Semanal", @@ -10996,11 +10996,11 @@ "945865332e": "Iniciando sesión" }, "UsagePage": { - "64265cb295": "71% 5h", + "64265cb295": "29% usado 5h", "be5a165875": "Cambiar a", "277a9c65a9": "Cuenta de Codex", "4dce5ca3aa": "Se restablece en 4d 3h", - "05ce4ecdd3": "62% restante", + "05ce4ecdd3": "38% usado", "0470aaed99": "Semanal", "f421abf962": "Sesión", "5e45fb1238": "Actualizado hace 1 min", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 59b3e7640..eb83299b3 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -2999,9 +2999,9 @@ "2483c60695": "・・・", "c35af53b73": "サインイン", "f19a63e7cd": "サインインして使用状況を確認する", - "5c938d39ac": "%週", - "54e8d6bb2d": "% Fable", - "d79c3362c4": "% 5時間", + "5c938d39ac": "週", + "54e8d6bb2d": "Fable", + "d79c3362c4": "5時間", "a79c64f87e": "Fable", "8295903d17": "切り替え後に古い会話を続ける前に、ライブ Claude terminals を再起動します。", "c98ea88392": "他のアカウントはありません", @@ -3169,7 +3169,7 @@ } }, "tooltip": { - "cedb7b99e3": "% 左", + "cedb7b99e3": "% 使用", "6d6df77f41": "利用可能なデータがありません", "7f7f208060": "毎月", "252c096536": "毎週", @@ -10996,11 +10996,11 @@ "945865332e": "サインインする" }, "UsagePage": { - "64265cb295": "71% 5時間", + "64265cb295": "29% 使用 5時間", "be5a165875": "に切り替えます", "277a9c65a9": "Codex アカウント", "4dce5ca3aa": "4d 3h でリセット", - "05ce4ecdd3": "62%残っています", + "05ce4ecdd3": "38%使用", "0470aaed99": "毎週", "f421abf962": "セッション", "5e45fb1238": "1分前に更新されました", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 3a81820ec..29d25e04b 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -2999,9 +2999,9 @@ "2483c60695": "···", "c35af53b73": "로그인", "f19a63e7cd": "사용량을 보려면 로그인하세요.", - "5c938d39ac": "%주간", - "54e8d6bb2d": "% Fable", - "d79c3362c4": "% 5h", + "5c938d39ac": "주간", + "54e8d6bb2d": "Fable", + "d79c3362c4": "5h", "a79c64f87e": "Fable", "8295903d17": "전환 후 이전 대화를 계속하기 전에 라이브 Claude terminals을 다시 시작하십시오.", "c98ea88392": "다른 계정 없음", @@ -3169,7 +3169,7 @@ } }, "tooltip": { - "cedb7b99e3": "% 남음", + "cedb7b99e3": "% 사용", "6d6df77f41": "데이터가 없습니다", "7f7f208060": "월간", "252c096536": "주간", @@ -10996,11 +10996,11 @@ "945865332e": "로그인 중" }, "UsagePage": { - "64265cb295": "71% 5시간", + "64265cb295": "29% 사용 5시간", "be5a165875": "다음으로 전환", "277a9c65a9": "Codex 계정", "4dce5ca3aa": "4일 3시간 후에 재설정됩니다.", - "05ce4ecdd3": "62% 남음", + "05ce4ecdd3": "38% 사용", "0470aaed99": "주간", "f421abf962": "세션", "5e45fb1238": "1분 전에 업데이트됨", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index ec6987a59..e8c2c8a10 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -2999,9 +2999,9 @@ "2483c60695": "···", "c35af53b73": "登录", "f19a63e7cd": "登录查看使用情况", - "5c938d39ac": "% 周", - "54e8d6bb2d": "% Fable", - "d79c3362c4": "% 5小时", + "5c938d39ac": "周", + "54e8d6bb2d": "Fable", + "d79c3362c4": "5小时", "a79c64f87e": "Fable", "8295903d17": "切换后继续旧对话之前,请重新启动实时 Claude 终端。", "c98ea88392": "没有其他账户", @@ -3169,7 +3169,7 @@ } }, "tooltip": { - "cedb7b99e3": "剩余 %", + "cedb7b99e3": "% 已用", "6d6df77f41": "无可用数据", "7f7f208060": "每月", "252c096536": "每周", @@ -10996,11 +10996,11 @@ "945865332e": "登录" }, "UsagePage": { - "64265cb295": "71% 5小时", + "64265cb295": "29% 已用 5小时", "be5a165875": "切换到", "277a9c65a9": "Codex 账户", "4dce5ca3aa": "4d 3h后重置", - "05ce4ecdd3": "还剩 62%", + "05ce4ecdd3": "已用 38%", "0470aaed99": "每周", "f421abf962": "会话", "5e45fb1238": "更新于 1 分钟前",