fix(ui): mark estimated headline totals with ~ on every hero (#1722)

#1657 put the ~ mark on estimated rows, but the headline total stayed bare on
the desktop hero, the mac hero, the Windows hero and tray text, and GNOME.
Each now applies the same 1% rule to the provider split's estimated portion
(the desktop helper moves from the VS Code summary into lib/format). Combined
device totals carry no estimated split and stay unmarked.
This commit is contained in:
Resham Joshi 2026-10-10 07:05:58 +02:00 • committed by GitHub
parent c2cfbd18d8
commit 389c14ce5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 67 additions and 25 deletions

View file

@ -1,4 +1,5 @@
import { localeTag, t } from '../i18n'
import type { MenubarPayload } from './types'
export type ActiveCurrency = { code: string; symbol: string; rate: number }
@ -24,6 +25,16 @@ export function isEstimatedCost(cost: number, estimatedCost: number | undefined)
return estimated > 0 && estimated >= cost * 0.01 && /[1-9]/.test(formatUsd(cost))
}
/** The estimated share of a payload's total: the provider split carries it on
* current CLIs; the model split is the fallback for a CLI that predates that. */
export function estimatedPortion(current: MenubarPayload['current']): number {
const providers = current.providerDetails?.filter(row => !row.excludedFromTotal)
if (providers?.some(row => row.estimatedCostUSD !== undefined)) {
return providers.reduce((sum, row) => sum + (row.estimatedCostUSD ?? 0), 0)
}
return (current.topModels ?? []).reduce((sum, row) => sum + (row.estimatedCostUSD ?? 0), 0)
}
/** Raw-USD input formatted against an explicit payload currency. This keeps a
* persisted exact snapshot correct on its very first paint, before App's
* global active-currency effect has had a chance to run. */

View file

@ -322,6 +322,23 @@ describe('Overview', () => {
expect(ticks.at(-1)).toHaveTextContent(now.toLocaleString('en-US', { month: 'short', day: 'numeric' }))
})
it.each([
['marks', 12.4, '~$312.40'],
['does not mark', 2, '$312.40'],
])('%s the hero total by the shared 1%% estimated rule', async (_label, estimatedCostUSD, shown) => {
const payload = makePayload(new Date())
payload.current.providerDetails = [
{ id: 'claude', label: 'Claude', cost: 300 - estimatedCostUSD },
{ id: 'cursor', label: 'Cursor', cost: 12.4 + estimatedCostUSD, estimatedCostUSD },
{ id: 'gateway', label: 'Gateway', cost: 50, estimatedCostUSD: 50, excludedFromTotal: true },
]
getOverview.mockResolvedValue(payload)
const { container } = render(<Overview period="30days" provider="all" />)
await waitFor(() => expect(container.querySelector('.ov-hero-num')).toHaveTextContent(new RegExp(`^${shown.replace(/[$.]/g, '\\$&')}$`)))
})
it('keeps weekday labels, and month labels on one row at least three columns apart', async () => {
const now = new Date()
getOverview.mockResolvedValue(makePayload(now))

View file

@ -11,7 +11,7 @@ import { StaleBanner } from '../components/StaleBanner'
import { DUR, motionEnabled, useBarGrowIn } from '../lib/motion'
import { useOptimizeSnapshot } from '../hooks/useOptimizeSnapshot'
import { type Polled, usePolled } from '../hooks/usePolled'
import { asOfLabel, formatCompact, formatCount, formatUsd, formatUsdWithCurrency, isEstimatedCost } from '../lib/format'
import { asOfLabel, estimatedPortion, formatCompact, formatCount, formatUsd, formatUsdWithCurrency, isEstimatedCost } from '../lib/format'
import { Usd, sumTokens, tokensOf, useUsdPop } from '../components/Usd'
import { codeburn } from '../lib/ipc'
import {
@ -598,8 +598,9 @@ function streakDays(daily: DailyHistoryEntry[], now: Date): number {
* changes (a user action), but never on the 30s poll: a value that arrives
* under the same `animateKey` snaps in place instead of re-animating.
*/
function CountUp({ value, tokens, animateKey, animate = true }: { value: number; tokens?: ReturnType<typeof tokensOf>; animateKey: string; animate?: boolean }) {
const pop = useUsdPop<HTMLDivElement>(tokens)
function CountUp({ value, tokens, animateKey, animate = true, estimated = false }: { value: number; tokens?: ReturnType<typeof tokensOf>; animateKey: string; animate?: boolean; estimated?: boolean }) {
const pop = useUsdPop<HTMLDivElement>(tokens, false, estimated)
const mark = estimated ? '~' : ''
const ref = pop.ref
const keyRef = useRef<string | null>(null)
@ -609,7 +610,7 @@ function CountUp({ value, tokens, animateKey, animate = true }: { value: number;
const keyChanged = keyRef.current !== animateKey
keyRef.current = animateKey
if (!animate || !keyChanged || !motionEnabled()) {
element.textContent = formatUsd(value)
element.textContent = mark + formatUsd(value)
return
}
const counter = { n: 0 }
@ -617,14 +618,14 @@ function CountUp({ value, tokens, animateKey, animate = true }: { value: number;
n: value,
duration: 0.7,
ease: 'power2.out',
onUpdate: () => { element.textContent = formatUsd(counter.n) },
onUpdate: () => { element.textContent = mark + formatUsd(counter.n) },
})
return () => { tween.kill() }
}, [value, animateKey, animate])
}, [value, animateKey, animate, mark])
return (
<>
<div ref={ref} className="ov-hero-num" data-countup={value} data-countup-animation={animate ? 'enabled' : 'suppressed'} {...pop.props}>{formatUsd(value)}</div>
<div ref={ref} className="ov-hero-num" data-countup={value} data-countup-animation={animate ? 'enabled' : 'suppressed'} {...pop.props}>{mark}{formatUsd(value)}</div>
{pop.pop}
</>
)
@ -1098,6 +1099,10 @@ export function OverviewContent({
const useGeneration = headline != null && genModels != null
const heroCost = combined ? combined.combined.cost : useGeneration ? headline.cost : data.current.cost
const heroCalls = combined ? combined.combined.calls : useGeneration ? headline.calls : data.current.calls
// The combined aggregate carries no estimated split, so it is never marked.
const heroEstimated = !combined && isEstimatedCost(heroCost, useGeneration
? genModels.reduce((sum, model) => sum + (model.estimatedCostUSD ?? 0), 0)
: estimatedPortion(data.current))
const heroSessions = combined ? combined.combined.sessions : data.current.sessions
const heroSessionLabel = combined
? formatCombinedSessionCount()
@ -1200,7 +1205,7 @@ export function OverviewContent({
{/* A returning launch already showed a truthful persisted headline.
Replaying the live hero from $0 on handoff makes that exact value
appear to collapse and recover; snap to the revalidated total. */}
<CountUp value={heroCost} tokens={heroTokens} animateKey={animateKey} animate={!suppressHeroReplay} />
<CountUp value={heroCost} tokens={heroTokens} animateKey={animateKey} animate={!suppressHeroReplay} estimated={heroEstimated} />
<div className="ov-hero-sub" title={heroSessionHelp}>{formatCount(heroCalls, 'call')} · {heroSessionLabel}</div>
{(provider === 'cursor' || provider === 'cursor-agent') && <CursorSyncLine status={data.cursorSync} />}
{combined

View file

@ -1,5 +1,5 @@
import { t } from '../i18n'
import { formatCount, formatResetTime, formatUsd, isEstimatedCost, setActiveCurrency, type ActiveCurrency } from '../lib/format'
import { estimatedPortion, formatCount, formatResetTime, formatUsd, isEstimatedCost, setActiveCurrency, type ActiveCurrency } from '../lib/format'
import { PROVIDER_NAMES } from '../lib/providers'
import type { MenubarPayload, ProviderName, QuotaProvider } from '../lib/types'
@ -24,16 +24,6 @@ export type Summary = {
const USD: ActiveCurrency = { code: 'USD', symbol: '$', rate: 1 }
/** The estimated share of a payload's total: the provider split carries it on
* current CLIs; the model split is the fallback for a CLI that predates that. */
function estimatedPortion(current: MenubarPayload['current']): number {
const providers = current.providerDetails?.filter(row => !row.excludedFromTotal)
if (providers?.some(row => row.estimatedCostUSD !== undefined)) {
return providers.reduce((sum, row) => sum + (row.estimatedCostUSD ?? 0), 0)
}
return (current.topModels ?? []).reduce((sum, row) => sum + (row.estimatedCostUSD ?? 0), 0)
}
/** The #1657 rule, as every other surface applies it. Needs the payload's currency active. */
export function figureOf(payload: MenubarPayload): Figure {
return { cost: payload.current.cost, estimated: isEstimatedCost(payload.current.cost, estimatedPortion(payload.current)) }

View file

@ -99,6 +99,13 @@ function formatCost(value, currency, rate = 1, exact = false) {
return `${symbol}${parts.join('.')}`;
}
// Same rule as the CLI (src/format.ts isEstimatedCost): `~` once the estimated
// portion is at least 1% of the figure, unless `shown` reads as zero.
function isEstimatedCost(cost, estimatedCostUSD, shown) {
const estimated = Number(estimatedCostUSD) || 0;
return estimated > 0 && estimated >= cost * 0.01 && /[1-9]/.test(shown);
}
function formatTokensCompact(n) {
const v = Number(n) || 0;
if (v >= 1_000_000_000) return `${(v / 1_000_000_000).toFixed(1)}B`;
@ -795,7 +802,10 @@ class CodeBurnIndicator extends PanelMenu.Button {
this._panelLabel.set_text(this._fmt(cost));
this._heroLabel.set_text(combined ? `Combined · ${current.label || ''}` : (current.label || ''));
this._heroAmount.set_text(this._fmt(cost));
// Combined totals carry no estimated split, so only a local figure is marked.
const estimated = !combined && current === payload?.current
&& isEstimatedCost(cost, (current.providerDetails ?? []).reduce((sum, d) => sum + numberValue(d?.estimatedCostUSD), 0), this._fmt(cost));
this._heroAmount.set_text(`${estimated ? '~' : ''}${this._fmt(cost)}`);
const calls = Number(combined?.calls ?? current.calls ?? 0);
const sessions = Number(combined?.sessions ?? current.sessions ?? 0);

View file

@ -104,7 +104,11 @@ struct HeroSection: View {
if total >= 1_000 { return String(format: "%.0fK tok", total / 1_000) }
return String(format: "%.0f tok", total)
}
return totals.cost.asCurrency()
let shown = totals.cost.asCurrency()
// The combined aggregate carries no estimated split, so it is never marked.
guard combinedUsage == nil else { return shown }
let estimated = store.payload.current.providerDetails.reduce(0.0) { $0 + ($1.estimatedCostUSD ?? 0) }
return (isEstimatedCost(totals.cost, estimated, shown: shown) ? "~" : "") + shown
}
private var combinedUsage: CombinedUsage? {

View file

@ -4,7 +4,7 @@ import { listen } from '@tauri-apps/api/event'
import type { MenubarPayload } from './lib/payload'
import type { CurrencyState } from './lib/currency'
import { USD, formatCurrency, formatTokens, trayBadgeText } from './lib/currency'
import { USD, formatCurrency, formatTokens, isEstimatedCost, trayBadgeText } from './lib/currency'
import { PayloadCache, sameSelection, selectionKey, type Selection } from './lib/cache'
import { relativePast } from './lib/dates'
import { applyTheme, readSetting, writeSetting } from './lib/settings'
@ -423,9 +423,11 @@ export function App() {
const isTokenMetric = settings.metric === 'tokens' || settings.metric === 'totalTokens'
// The 16 px badge bitmap has room for about four glyphs, so the mac's up/down token pair
// cannot fit; both token metrics show the total there and the hero carries the split.
const trayEstimated = trayCombined === null && trayCost !== null
&& isEstimatedCost(trayCost, (trayCurrent?.providerDetails ?? []).reduce((sum, d) => sum + (d.estimatedCostUSD ?? 0), 0), formatCurrency(trayCost, currency))
const trayFigure = isTokenMetric
? (trayTokens === null ? null : `${formatTokens(trayTokens)} tok`)
: (trayCost === null ? null : formatCurrency(trayCost, currency))
: (trayCost === null ? null : `${trayEstimated ? '~' : ''}${formatCurrency(trayCost, currency)}`)
// Combined scope with a paired device that did not report this cycle: the figure is short
// of what these machines actually spent, and the mac says so beside its title with a dimmed
// "reachable/total". A 16 px bitmap has no room for that, so the badge is drawn dimmed and

View file

@ -1,6 +1,6 @@
import type { CombinedUsage, CursorSyncStatus, MenubarPayload } from '../lib/payload'
import type { CurrencyState } from '../lib/currency'
import { formatCurrency, formatTokens } from '../lib/currency'
import { formatCurrency, formatTokens, isEstimatedCost } from '../lib/currency'
import { prettyDate, relativePast, todayKey } from '../lib/dates'
import { SectionCaption } from './CollapsibleSection'
import { ArrowDownRight, ArrowUpRight, LeafIcon, MonitorIcon, WarningIcon } from './Icons'
@ -50,9 +50,12 @@ export function HeroSection({ payload, currency, periodLabel, isToday, dailyBudg
// Both token metrics put the total in the headline, as the mac's heroText does; only the
// Tokens metric replaces calls and sessions with the up and down split.
const isTokenMetric = metric === 'tokens' || metric === 'totalTokens'
const shownCost = formatCurrency(cost, currency)
// The combined aggregate carries no estimated split, so it is never marked.
const estimated = !totals && isEstimatedCost(cost, (payload?.current.providerDetails ?? []).reduce((sum, d) => sum + (d.estimatedCostUSD ?? 0), 0), shownCost)
const headline = isTokenMetric
? `${formatTokens(inputTokens + outputTokens)} tok`
: formatCurrency(cost, currency)
: `${estimated ? '~' : ''}${shownCost}`
const label = payload?.current.label || periodLabel
const caption = combined ? `Combined · ${label}` : isToday ? `Today · ${todayLabel}` : label