(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 (
<>
- {formatUsd(value)}
+ {mark}{formatUsd(value)}
{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. */}
-
+
{formatCount(heroCalls, 'call')} · {heroSessionLabel}
{(provider === 'cursor' || provider === 'cursor-agent') && }
{combined
diff --git a/app/renderer/vscode/summary.ts b/app/renderer/vscode/summary.ts
index 93cd5290..b088f920 100644
--- a/app/renderer/vscode/summary.ts
+++ b/app/renderer/vscode/summary.ts
@@ -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)) }
diff --git a/gnome/indicator.js b/gnome/indicator.js
index d11c9b90..bad6f151 100644
--- a/gnome/indicator.js
+++ b/gnome/indicator.js
@@ -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);
diff --git a/mac/Sources/CodeBurnMenubar/Views/HeroSection.swift b/mac/Sources/CodeBurnMenubar/Views/HeroSection.swift
index 6109ba1a..80ffb5ad 100644
--- a/mac/Sources/CodeBurnMenubar/Views/HeroSection.swift
+++ b/mac/Sources/CodeBurnMenubar/Views/HeroSection.swift
@@ -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? {
diff --git a/windows/src/App.tsx b/windows/src/App.tsx
index b8185569..84e1e85b 100644
--- a/windows/src/App.tsx
+++ b/windows/src/App.tsx
@@ -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
diff --git a/windows/src/components/HeroSection.tsx b/windows/src/components/HeroSection.tsx
index 4baacc8b..ee08194e 100644
--- a/windows/src/components/HeroSection.tsx
+++ b/windows/src/components/HeroSection.tsx
@@ -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