SLOPSHOPPER

usage-line

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

newstatustimer
v0.1.0MITupdated 2026-10-07tusharranjan-ai/usage-line
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-line
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ usage-line: Opus 5.5 · 🟡 ctx ▰▰▰▰▰▱▱▱▱▱ 49% 97k/200k · $0.42 · 5h 31%
README

usage-line

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

demo

Illustration of the line as context fills, not a screen recording.

  • Context bar: 🟢 under 30%, 🟡 30–70%, 🔴 over 70%
  • cache: prompt-cache hit rate for this session (resets on a mod reload)
  • $: session cost, as /cost totals it
  • 5h / 7d: rate-limit usage with time to reset

Needs Claude Code 2.1.287 or later (mods).

Install

/plugin install usage-line --marketplace tusharranjan-ai/usage-line

Answer y to add the marketplace, then pick a scope.

Notes

The status line is one fixed color, so colors come from emoji, not ANSI.

License

MIT

Source 1 files
hooks/register.ts 63 lines
1import 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