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:
megaproxy 2026-05-28 22:43:06 +01:00
parent b23f3d1ecb
commit d951c360ae
12 changed files with 235 additions and 612 deletions

View file

@ -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}`;
}