codeburn/app/renderer/components/Usd.tsx
Resham Joshi b2be8eba56
feat: mark estimated dollars the same way in every view (#1657)
* Mark estimated costs with the same ~ in every view

The ~ marker and legend that report already used now appear wherever a
dollar figure is mostly a guess: the models and sessions CLI output, the
desktop Models, Overview, Sessions and Settings provider rows, the macOS
menu bar model rows and provider tabs, and the Windows tray.

One rule decides the marker everywhere: the estimated portion is at least
1% of the figure. report, overview and the MCP tables switch to it from
"any estimated cent", so a metered Codex row holding a few text-estimated
turns stays unmarked.

JSON is additive: estimatedCostUSD and isEstimated on models rows,
estimatedCost and isEstimated on sessions rows, estimatedCostUSD on
menubar providerDetails rows. Totals are unchanged.

* Skip the ~ marker on figures that print as zero

A sub-cent row that is fully estimated printed as "~$0.00": nothing visible
to qualify. The shared helper now also requires the amount, as the surface
prints it in the active currency, to show a non-zero digit. CLI, desktop,
macOS and Windows helpers all apply it.
2026-10-06 22:27:13 +02:00

125 lines
5.5 KiB
TypeScript

import { useId, useRef, useState, type ReactElement, type RefObject } from 'react'
import { AnchoredSurface } from './AnchoredSurface'
import { useEscape } from '../hooks/useEscape'
import { t } from '../i18n'
import { formatCompact, formatUsd } from '../lib/format'
/** The token counts behind one dollar amount. `calls` is optional context. */
export type TokenBreakdown = {
inputTokens: number
outputTokens: number
cacheReadTokens: number
cacheWriteTokens: number
calls?: number
}
type PartialBreakdown = {
inputTokens?: number
outputTokens?: number
cacheReadTokens?: number
cacheWriteTokens?: number
calls?: number
}
/** A breakdown only when every count is present — an older payload omits them,
* and absence must show nothing rather than a zero that reads as measured. */
export function tokensOf(source: PartialBreakdown | null | undefined): TokenBreakdown | null {
if (!source) return null
const { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens } = source
if (inputTokens == null || outputTokens == null || cacheReadTokens == null || cacheWriteTokens == null) return null
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, calls: source.calls }
}
/** Totals over rows behind an aggregate amount; `divideBy` turns a sum into the
* average that an Avg/day figure is. */
export function sumTokens(rows: readonly PartialBreakdown[], divideBy = 1): TokenBreakdown | null {
if (!rows.length || divideBy <= 0) return null
const total = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
// One row without a call count makes the total unknown, not smaller: adding a
// zero for it would print a Calls row that no source measured.
let calls: number | undefined = 0
for (const row of rows) {
const t = tokensOf(row)
if (!t) return null
total.inputTokens += t.inputTokens
total.outputTokens += t.outputTokens
total.cacheReadTokens += t.cacheReadTokens
total.cacheWriteTokens += t.cacheWriteTokens
if (t.calls == null) calls = undefined
else if (calls !== undefined) calls += t.calls
}
return {
inputTokens: Math.round(total.inputTokens / divideBy),
outputTokens: Math.round(total.outputTokens / divideBy),
cacheReadTokens: Math.round(total.cacheReadTokens / divideBy),
cacheWriteTokens: Math.round(total.cacheWriteTokens / divideBy),
...(calls === undefined ? {} : { calls: Math.round(calls / divideBy) }),
}
}
/**
* Hover/focus popover of the tokens behind a dollar amount, for an element that
* renders its own text. `Usd` is the ordinary case; the hero's count-up owns its
* text node, so it takes the parts directly.
*/
export function useUsdPop<T extends HTMLElement>(tokens: TokenBreakdown | null | undefined, nested = false, estimated = false): {
ref: RefObject<T | null>
props: Record<string, unknown>
pop: ReactElement | null
} {
const ref = useRef<T>(null)
const surfaceRef = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(false)
const id = useId()
useEscape(open, () => setOpen(false))
if (!tokens) return { ref, props: {}, pop: null }
// Inside an interactive row (a session-row button) the trigger must not add its
// own tab stop or steal the row's Enter, so there it is hover/mouse-only and
// out of the tab order; standalone it stays keyboard-focusable.
return {
ref,
props: {
'data-usd': '',
tabIndex: nested ? -1 : 0,
'aria-describedby': open ? id : undefined,
onMouseEnter: () => setOpen(true),
onMouseLeave: () => setOpen(false),
...(nested ? {} : { onFocus: () => setOpen(true), onBlur: () => setOpen(false) }),
},
pop: open ? (
<AnchoredSurface anchor={ref} surfaceRef={surfaceRef} id={id} className="pop-menu usd-pop" role="tooltip">
{estimated && <div className="usd-pop-row"><span>{t('shared.usd.estimated')}</span></div>}
<TokenRows tokens={tokens} />
</AnchoredSurface>
) : null,
}
}
/** The four counts, plus calls when the source carries them. */
export function TokenRows({ tokens }: { tokens: TokenBreakdown }): ReactElement {
return (
<>
<div className="usd-pop-row"><span>{t('shared.usd.input')}</span><b>{formatCompact(tokens.inputTokens)}</b></div>
<div className="usd-pop-row"><span>{t('shared.usd.output')}</span><b>{formatCompact(tokens.outputTokens)}</b></div>
<div className="usd-pop-row"><span>{t('shared.usd.cacheRead')}</span><b>{formatCompact(tokens.cacheReadTokens)}</b></div>
<div className="usd-pop-row"><span>{t('shared.usd.cacheWrite')}</span><b>{formatCompact(tokens.cacheWriteTokens)}</b></div>
{tokens.calls != null && <div className="usd-pop-row calls"><span>{t('shared.usd.calls')}</span><b>{formatCompact(tokens.calls)}</b></div>}
</>
)
}
/** A dollar amount that reveals its token breakdown on hover or focus. Without
* `tokens` it is the plain formatted amount and nothing else. `estimated`
* prefixes the `~` marker and names it on hover: in the token popover when
* there is one, else as the native title. */
export function Usd({ value, tokens, className, nested, estimated }: { value: number; tokens?: TokenBreakdown | null; className?: string; nested?: boolean; estimated?: boolean }): ReactElement {
const pop = useUsdPop<HTMLSpanElement>(tokens, nested, estimated)
const title = estimated && !tokens ? t('shared.usd.estimated') : undefined
return (
<>
<span ref={pop.ref} className={className} title={title} {...pop.props}>{estimated ? '~' : ''}{formatUsd(value)}</span>
{pop.pop}
</>
)
}