One status line: model, context bar (🟢🟡🔴), cache hit rate, session cost, and 5h/7d limits with reset countdowns.

A Claude Code mod: one status line under the prompt.
Sonnet 5.5 · 🟢 ctx ▰▱▱▱▱▱▱▱▱▱ 12% 116k/1M · cache 94% · $1.35 · 5h 5% ↻4h41m · 7d 5% ↻6d5h

Illustration of the line as context fills, not a screen recording.
/cost totals itNeeds Claude Code 2.1.287 or later (mods).
/plugin install usage-line --marketplace tusharranjan-ai/usage-line
Answer y to add the marketplace, then pick a scope.
The status line is one fixed color, so colors come from emoji, not ANSI.
MIT
hooks/register.ts 63 lines1import type { EngineInterface, Register } from 'claude-code'
2
3const eta = (iso: string | undefined, now: number) => {
4 const s = Math.floor((Date.parse(iso ?? '') - now) / 1000)
5 if (!(s > 0)) return ''
6 const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60)
7 return ` ↻${d > 0 ? `${d}d${h}h` : `${h}h${m}m`}`
8}
9const pretty = (id: string) => {
10 const m = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?/.exec(id)
11 return m ? `${m[1]![0]!.toUpperCase()}${m[1]!.slice(1)} ${m[2]}${m[3] ? `.${m[3]}` : ''}` : id
12}
13const k = (n: number) => (n >= 1e6 ? `${+(n / 1e6).toFixed(1)}M` : `${Math.round(n / 1e3)}k`)
14
15type Usage = { cost?: { usd: number }; context: { percent?: number; tokens?: number; window?: number }; rateLimits: { kind: string; percentUsed: number; resetsAt?: string }[] }
16
17// ponytail: module vars, so a hot reload zeroes the hit rate; move to $.state if that bites
18let read = 0, total = 0
19
20const resets: Record<string, string> = {}
21
22export const line = (u: Usage, model: string, now: number) => {
23 for (const r of u.rateLimits) if (r.resetsAt) resets[r.kind] = r.resetsAt
24 const lim = (label: string, kind: string) => {
25 const r = u.rateLimits.find(l => l.kind === kind)
26 return r ? `${label} ${Math.round(r.percentUsed)}%${eta(r.resetsAt ?? resets[kind], now)}` : undefined
27 }
28 const c = u.context
29 const pct = c.percent ?? 0
30 const on = Math.min(10, Math.round(pct / 10))
31 const dot = pct < 30 ? '🟢' : pct <= 70 ? '🟡' : '🔴' // status text is one fixed color; emoji carry the color
32 const ctx = c.percent === undefined ? undefined : `${dot} ctx ${'▰'.repeat(on)}${'▱'.repeat(10 - on)} ${Math.round(pct)}% ${k(c.tokens ?? 0)}/${k(c.window ?? 0)}`
33 const hit = total ? `cache ${Math.round((100 * read) / total)}%` : undefined
34 const cost = u.cost ? `$${u.cost.usd.toFixed(2)}` : undefined
35 return [pretty(model), ctx, hit, cost, lim('5h', 'five_hour'), lim('7d', 'seven_day')].filter(Boolean).join(' · ')
36}
37
38async function draw($: EngineInterface) {
39 $.ui.status(line(await $.session.usage(), (await $.session.model()) || '', await $.clock.now()))
40}
41
42export const register: Register = on => {
43 on('session.start', ($, e, next) => {
44 $.clock.every(30_000, () => void draw($)) // limits move between turns; ponytail: poll, no push event
45 void draw($)
46 return next(e)
47 })
48 on('turn.step', async function* ($, e, next) {
49 const r = yield* next(e)
50 const u = r.usage
51 if (u) {
52 read += u.cache_read_input_tokens
53 total += u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens
54 }
55 return r
56 })
57 on('session.measure', async ($, e, next) => {
58 const r = await next(e)
59 $.ui.status(line(e, (await $.session.model()) || '', await $.clock.now())) // measure carries resetsAt
60 return r
61 })
62}
63