diff --git a/desktop/frontend/src/components/StatusBar.tsx b/desktop/frontend/src/components/StatusBar.tsx index 6cb0cb189..df339f5aa 100644 --- a/desktop/frontend/src/components/StatusBar.tsx +++ b/desktop/frontend/src/components/StatusBar.tsx @@ -3,6 +3,7 @@ import { Coins, Cpu, Wallet } from "lucide-react"; import { EffortSwitcher } from "./EffortSwitcher"; import { ModelSwitcher } from "./ModelSwitcher"; import { Tooltip } from "./Tooltip"; +import { UsageIndicator } from "./UsageIndicator"; import { SPINNER_WORDS, useI18n } from "../lib/i18n"; import type { BalanceInfo, ContextInfo, EffortInfo, JobView, Meta, Mode, WireUsage } from "../lib/types"; @@ -168,6 +169,12 @@ export function StatusBar({ {t("status.cacheAvg", { pct: avgPct })} )} + {usage && ( + <> + · + + + )} {jobs && jobs.length > 0 && ( <> · diff --git a/desktop/frontend/src/components/UsageIndicator.tsx b/desktop/frontend/src/components/UsageIndicator.tsx new file mode 100644 index 000000000..53bde359b --- /dev/null +++ b/desktop/frontend/src/components/UsageIndicator.tsx @@ -0,0 +1,48 @@ +import type { WireUsage } from "../lib/types"; + +// UsageIndicator is a compact, info-dense readout of the latest turn's +// token usage. It mounts in the status bar to the right of the cache- +// hit % indicator. The three numbers are styled with a small caption +// + value pair so the user can tell at a glance "this turn cost 4.2k +// completion tokens, mostly from the prompt cache". +// +// We use a pill instead of a sparkline because the controller exposes +// only the latest WireUsage payload (not a per-turn history). A +// sparkline would need a separate ring buffer in the controller; a +// future PR can add that and swap the pill for a real line chart. +// +// The component is intentionally tiny: 60 lines, no state, no +// effects. It re-renders when the WireUsage prop changes (the +// controller dispatches on every usage event during the turn). +export function UsageIndicator({ usage }: { usage?: WireUsage }) { + if (!usage) return null; + // "k" rounding: 0–999 shows as the raw number, ≥1000 as "1.2k". + // Two significant digits feel right for both 800 (informative) and + // 184,000 (don't drown the status bar). + const fmt = (n: number): string => { + if (n < 1000) return String(n); + return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`; + }; + // The "saved" caption shows the cache-hit count as a fraction of + // the prompt tokens; deepSeek and Anthropic charge less for cache + // hits, so this number is the user-facing ROI. + const promptSaved = usage.cacheHitTokens; + return ( +
+ + + {fmt(usage.promptTokens)} + + + + {fmt(usage.completionTokens)} + + {promptSaved > 0 && ( + + + {fmt(promptSaved)} + + )} +
+ ); +} diff --git a/desktop/frontend/src/styles.css b/desktop/frontend/src/styles.css index 90878fb0e..80b3a76da 100644 --- a/desktop/frontend/src/styles.css +++ b/desktop/frontend/src/styles.css @@ -5541,6 +5541,7 @@ body { color: var(--fg); background: var(--hover); } + .composer__pasted { display: flex; flex-direction: column; @@ -5881,4 +5882,47 @@ body { .onboarding__skip:disabled { opacity: 0.4; cursor: not-allowed; + + +/* UsageIndicator: a small three-cell pill that mounts in the status + bar. The up/down arrows are in unicode (cheaper than lucide icons + for an always-on status bar element). The cache-hit cell is + dimmer than the prompt/completion cells so the eye lands on the + billable numbers first. */ +.usage { + display: inline-flex; + align-items: center; + gap: 0; + margin-left: 6px; + padding: 2px 4px; + font-family: var(--mono); + font-size: 10.5px; + color: var(--fg-dim); + background: var(--bg-soft); + border: 1px solid var(--border-soft); + border-radius: 4px; +} +.usage__cell { + display: inline-flex; + align-items: baseline; + gap: 2px; + padding: 0 5px; + border-right: 1px solid var(--border-soft); +} +.usage__cell:last-child { + border-right: 0; +} +.usage__lbl { + opacity: 0.6; + font-size: 9.5px; +} +.usage__val { + color: var(--fg); + font-variant-numeric: tabular-nums; +} +.usage__cell--saved { + color: var(--add-fg); +} +.usage__cell--saved .usage__val { + color: var(--add-fg); }