Replace token-usage panel with per-pane context-fill indicator
For a subscription user, lifetime token totals + a $ estimate aren't
actionable; how full each session's context window is right now is. So:
- Removed the UsagePanel, the titlebar 💰 chip, and Ctrl+Shift+U.
- Repurposed the transcript reader (src-tauri/src/usage.rs): get_pane_context
returns each recent session's CURRENT context occupancy = the last
assistant turn's input + cache_read + cache_creation tokens (the prompt
size), instead of lifetime sums. Same UNC/$HOME/cache/recency machinery.
- src/lib/usage.ts now holds context helpers (window inference 200k vs 1M by
whether occupancy already exceeds 200k, % , green→amber→red ramp, label).
- App polls get_pane_context (15s, visibility-gated) into a cwd→context map
exposed via orchestration; each LeafPane looks itself up by leaf.cwd and
renders a slim fill bar + % in its header (hidden for non-claude/unmatched
panes).
Also fixes the narrow-pane toolbar: a ResizeObserver sets leaf--narrow /
leaf--xnarrow width tiers; the label shrinks first, split buttons / status /
secondary chips drop out by tier, and the close × + context indicator stay
pinned right and visible down to the 180px min width.
tsc clean (apart from the not-yet-installed xterm addons). Rust builds on
the Windows host; needs runtime verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b23f3d1ecb
commit
d951c360ae
12 changed files with 235 additions and 612 deletions
112
src/lib/usage.ts
112
src/lib/usage.ts
|
|
@ -1,81 +1,38 @@
|
|||
// Pricing + formatting helpers for the claude usage panel. Token tallies come
|
||||
// from the backend (src-tauri/src/usage.rs); cost is applied here so the rate
|
||||
// table is easy to edit without recompiling Rust.
|
||||
// Helpers for the per-pane context-fill indicator. Context occupancy (token
|
||||
// count) comes from the backend (src-tauri/src/usage.rs, get_pane_context); this
|
||||
// turns it into a window %, a colour, and a human label.
|
||||
|
||||
import type { SessionUsage } from "../ipc";
|
||||
import type { SessionContext } from "../ipc";
|
||||
|
||||
interface Rate {
|
||||
/** USD per million tokens. */
|
||||
input: number;
|
||||
output: number;
|
||||
cacheWrite: number;
|
||||
cacheRead: number;
|
||||
const WINDOW_STANDARD = 200_000;
|
||||
const WINDOW_LARGE = 1_000_000;
|
||||
|
||||
/**
|
||||
* Context-window size for a session. The transcript's model id doesn't encode
|
||||
* the 200k-vs-1M variant, so we infer: a session whose prompt has already
|
||||
* exceeded 200k must be running the 1M-context window. Approximate near the
|
||||
* boundary, but correct for the cases that matter (a small session reads
|
||||
* against 200k; a large one against 1M).
|
||||
*/
|
||||
export function contextWindow(contextTokens: number): number {
|
||||
return contextTokens > WINDOW_STANDARD ? WINDOW_LARGE : WINDOW_STANDARD;
|
||||
}
|
||||
|
||||
// Published Anthropic API rates, USD per million tokens, as of 2026-05.
|
||||
// UPDATE if pricing changes. Matched against the model id by substring.
|
||||
// cacheWrite uses the 5-minute-TTL rate (1.25× input); 1-hour cache writes
|
||||
// (2× input) are billed slightly higher than this estimate shows.
|
||||
const RATES: { match: string; rate: Rate }[] = [
|
||||
{ match: "opus", rate: { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
||||
{ match: "sonnet", rate: { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 } },
|
||||
{ match: "haiku", rate: { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 } },
|
||||
];
|
||||
// Unknown model → assume sonnet-tier rates (a middle-ground estimate).
|
||||
const FALLBACK_RATE = RATES[1].rate;
|
||||
|
||||
function rateFor(model: string): Rate {
|
||||
const m = model.toLowerCase();
|
||||
return RATES.find((r) => m.includes(r.match))?.rate ?? FALLBACK_RATE;
|
||||
/** Fraction (0..1) of the inferred window currently occupied. */
|
||||
export function contextFraction(s: SessionContext): number {
|
||||
const w = contextWindow(s.contextTokens);
|
||||
return w > 0 ? Math.min(1, s.contextTokens / w) : 0;
|
||||
}
|
||||
|
||||
/** Estimated USD cost for one session, summed per-model. */
|
||||
export function sessionCost(s: SessionUsage): number {
|
||||
let usd = 0;
|
||||
for (const mu of s.models) {
|
||||
const r = rateFor(mu.model);
|
||||
usd +=
|
||||
(mu.inputTokens * r.input +
|
||||
mu.outputTokens * r.output +
|
||||
mu.cacheCreationTokens * r.cacheWrite +
|
||||
mu.cacheReadTokens * r.cacheRead) /
|
||||
1_000_000;
|
||||
}
|
||||
return usd;
|
||||
export function contextPercent(s: SessionContext): number {
|
||||
return Math.round(contextFraction(s) * 100);
|
||||
}
|
||||
|
||||
/** Total tokens (all kinds) for one session. */
|
||||
export function sessionTokens(s: SessionUsage): number {
|
||||
let t = 0;
|
||||
for (const mu of s.models) {
|
||||
t += mu.inputTokens + mu.outputTokens + mu.cacheCreationTokens + mu.cacheReadTokens;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Short family name of the model that produced the most output in a session. */
|
||||
export function dominantModel(s: SessionUsage): string {
|
||||
let best: SessionUsage["models"][number] | undefined;
|
||||
for (const mu of s.models) {
|
||||
if (!best || mu.outputTokens > best.outputTokens) best = mu;
|
||||
}
|
||||
return best ? shortModel(best.model) : "—";
|
||||
}
|
||||
|
||||
export function shortModel(model: string): string {
|
||||
const m = model.toLowerCase();
|
||||
if (m.includes("opus")) return "opus";
|
||||
if (m.includes("sonnet")) return "sonnet";
|
||||
if (m.includes("haiku")) return "haiku";
|
||||
return model;
|
||||
}
|
||||
|
||||
export function totalCost(sessions: SessionUsage[]): number {
|
||||
return sessions.reduce((acc, s) => acc + sessionCost(s), 0);
|
||||
}
|
||||
|
||||
export function formatUsd(n: number): string {
|
||||
return "$" + n.toFixed(2);
|
||||
/** Green → amber → red ramp as the window fills. */
|
||||
export function contextColor(fraction: number): string {
|
||||
if (fraction >= 0.85) return "#d65a5a";
|
||||
if (fraction >= 0.6) return "#d6a23a";
|
||||
return "#5aa84a";
|
||||
}
|
||||
|
||||
export function formatTokens(n: number): string {
|
||||
|
|
@ -84,14 +41,9 @@ export function formatTokens(n: number): string {
|
|||
return String(n);
|
||||
}
|
||||
|
||||
/** `nowMs` is passed in so callers can avoid Date.now() churn in render. */
|
||||
export function relativeTime(ms: number, nowMs: number): string {
|
||||
const dt = Math.max(0, nowMs - ms);
|
||||
const s = Math.floor(dt / 1000);
|
||||
if (s < 60) return `${s}s ago`;
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m ago`;
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 24) return `${h}h ago`;
|
||||
return `${Math.floor(h / 24)}d ago`;
|
||||
/** e.g. "~274k / 1M" for a tooltip. */
|
||||
export function contextLabel(s: SessionContext): string {
|
||||
const w = contextWindow(s.contextTokens);
|
||||
const wLabel = w >= 1_000_000 ? "1M" : "200k";
|
||||
return `~${formatTokens(s.contextTokens)} / ${wLabel}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue