Linha de status compacta: modelo, branch, barras de contexto e limites 5h/7d, custo e estado do cache de prompt.

Mod do Claude Code que mostra o consumo da sessão numa linha de status compacta, abaixo do prompt:
Opus 5.5 ⎇ main │ ctx ▰▰▰▱▱▱▱▱ 42% │ 5h ▰▰▱▱▱▱▱▱ 23% 2h10 │ 7d ▰▰▰▰▰▱▱▱ 62% 3d4h │ $1.23 │ ⚡90% 4m
| Campo | O que é |
|---|---|
Opus 5.5 ⎇ main | modelo da sessão e branch do git |
ctx | janela de contexto ocupada |
5h / 7d | limites de uso da assinatura e tempo até resetar (vazio com API key) |
$1.23 | custo da sessão em USD |
⚡90% 4m / ❄90% | quanto da entrada do último turno veio do cache; quente com tempo restante, ou frio |
Atualiza a cada turno, a cada medição do Claude Code e a cada 15 s.
No prompt do Claude Code (terminal):
/plugin install usage-line --marketplace osceiatech/skills-osceia
Ou no shell:
claude plugin install usage-line@skills-osceia --scope user
Responda y para adicionar o marketplace e escolha o escopo. O mod passa a valer na hora.
Repositório privado: precisa de acesso de leitura ao osceiatech/skills-osceia e credencial git para o github.com (chave SSH, ou gh auth login seguido de gh auth setup-git).
/config → Prompt cache TTL (cacheTtl): 5m (padrão da API) ou 1h. Ajuste para o TTL da sua conta, senão o cache aparece frio antes da hora.
claude --plugin-dir ./usage-line # carrega da pasta, com recarga ao salvar
claude plugin test ./usage-line
claude plugin validate ./usage-linehooks/register.ts 122 lines1import type { EngineInterface, Register, SessionRateLimit, SessionUsage, TurnUsage } from 'claude-code'
2
3const SEP = ' │ '
4const TICK_MS = 15_000
5
6type Seen = {
7 ttlMs: number
8 lastUsage?: TurnUsage
9 lastResponseAt?: number
10 measured?: Pick<SessionUsage, 'context' | 'rateLimits' | 'cost'>
11}
12
13const seen: Seen = { ttlMs: 5 * 60_000 }
14
15export const register: Register = (on, options) => {
16 seen.ttlMs = options.cacheTtl === '1h' ? 60 * 60_000 : 5 * 60_000
17
18 on('session.start', async ($, e, next) => {
19 const started = await next(e)
20 await paint($)
21 $.clock.every(TICK_MS, () => void paint($))
22
23 return started
24 })
25
26 on('session.measure', async ($, e, next) => {
27 seen.measured = { context: e.context, rateLimits: e.rateLimits, cost: e.cost }
28 await paint($)
29
30 return next(e)
31 })
32
33 on('turn.complete', async ($, e, next) => {
34 if (e.agentId === undefined && e.usage !== undefined) {
35 seen.lastUsage = e.usage
36 seen.lastResponseAt = await $.clock.now()
37 await paint($)
38 }
39
40 return next(e)
41 })
42}
43
44async function paint($: EngineInterface) {
45 const now = await $.clock.now()
46 const [model, branch] = await Promise.all([$.session.model(), gitBranch($)])
47 const usage = seen.measured ?? (await $.session.usage())
48
49 const parts = [
50 branch === undefined ? prettyModel(model) : `${prettyModel(model)} ⎇ ${branch}`,
51 context(usage.context),
52 ...usage.rateLimits.map(l => limit(l, now)),
53 usage.cost === undefined ? undefined : `$${usage.cost.usd.toFixed(2)}`,
54 cache(seen.lastUsage, seen.lastResponseAt, now, seen.ttlMs),
55 ]
56
57 $.ui.status(parts.filter(p => p !== undefined).join(SEP))
58}
59
60async function gitBranch($: EngineInterface) {
61 try {
62 const { exitCode, stdout } = await $.process.run(['git', 'branch', '--show-current'], {
63 timeoutMs: 3000,
64 })
65 const name = stdout.trim()
66
67 return exitCode === 0 && name !== '' ? name : undefined
68 } catch {
69 return undefined
70 }
71}
72
73// claude-opus-5-5[1m] -> Opus 5.5 (1M)
74const prettyModel = (id: string) => {
75 const wide = id.includes('[1m]') ? ' (1M)' : ''
76 const words = id.replace(/\[1m\]$/, '').replace(/^claude-/, '').replace(/-\d{8}$/, '').split('-')
77 const family = words.filter(w => !/^\d+$/.test(w))
78 const version = words.filter(w => /^\d+$/.test(w)).join('.')
79 const name = family.map(w => w[0]?.toUpperCase() + w.slice(1)).join(' ')
80
81 return `${name}${version === '' ? '' : ` ${version}`}${wide}`
82}
83
84const BAR_CELLS = 8
85
86const bar = (percent: number) => {
87 const filled = Math.round((Math.min(100, Math.max(0, percent)) / 100) * BAR_CELLS)
88
89 return '▰'.repeat(filled) + '▱'.repeat(BAR_CELLS - filled)
90}
91
92const metric = (label: string, percent: number | undefined) =>
93 percent === undefined ? `${label} ${bar(0)} –` : `${label} ${bar(percent)} ${Math.round(percent)}%`
94
95const context = (c: SessionUsage['context']) => metric('ctx', c.percent)
96
97const duration = (ms: number) => {
98 const m = Math.max(0, Math.round(ms / 60_000))
99 if (m < 60) return `${m}m`
100 const h = Math.floor(m / 60)
101 if (h < 24) return `${h}h${String(m % 60).padStart(2, '0')}`
102
103 return `${Math.floor(h / 24)}d${h % 24}h`
104}
105
106const LIMIT_LABEL: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: '$lim' }
107
108const limit = (l: SessionRateLimit, now: number) => {
109 const reset = l.resetsAt === undefined ? '' : ` ${duration(Date.parse(l.resetsAt) - now)}`
110
111 return `${metric(LIMIT_LABEL[l.kind] ?? l.kind, l.percentUsed)}${reset}`
112}
113
114const cache = (u: TurnUsage | undefined, at: number | undefined, now: number, ttlMs: number) => {
115 if (u === undefined || at === undefined) return undefined
116 const input = u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens
117 const hit = input === 0 ? 0 : Math.round((u.cache_read_input_tokens / input) * 100)
118 const left = at + ttlMs - now
119
120 return left > 0 ? `⚡${hit}% ${duration(left)}` : `❄${hit}%`
121}
122