SLOPSHOPPER

usage-bars

Barras acima do prompt: contexto, sessão (5 horas) e semana, com detalhe no hover

newband
v0.1.0no licenseupdated 2026-10-16luismiggarcia/claude-usage-bars
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-bars
› 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 Contexto 49% Sessão 31% Semana — ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Contexto 49% Sessão 31% Semana —
README

usage-bars

Um mod para o Claude Code que mostra, numa linha imediatamente acima da caixa do prompt, três barras:

BarraO que medeDetalhe no hover
Contextoo contexto da sessão atual já ocupadotokens usados / janela
Sessãoa janela de uso de 5 horas da contaquanto falta para repor
Semanao limite semanal da contaquanto falta para repor

Cada barra tem a sua cor (azul, violeta, verde-água) e passa a vermelho a partir de 85%. No desktop as barras são pílulas finas em SVG; no terminal, blocos de cor.

Os valores de Sessão e Semana chegam com cada resposta da API: numa sessão parada ficam com o último valor que essa sessão viu.

Instalar

No prompt de uma sessão de terminal:

/plugin install usage-bars --marketplace luismiggarcia/claude-usage-bars

Numa shell:

claude plugin install usage-bars --marketplace luismiggarcia/claude-usage-bars

Na app desktop, onde /plugin não corre: clicar em + ao lado da caixa do prompt, depois Plugins, Add marketplace com luismiggarcia/claude-usage-bars, e Add plugin.

Instalado no âmbito do utilizador, carrega em todas as sessões novas.

Desenvolver

claude plugin validate .
claude plugin test .

© LG-Systems

Source 1 files
hooks/register.tsx 169 lines
1import type { Register, SessionContextUsage, SessionRateLimit } from 'claude-code'
2
3type Window = { percent: number; resetsAt?: string }
4
5type Usage = {
6  context: { percent?: number; tokens?: number; window: number }
7  fiveHour: Window | null
8  week: Window | null
9}
10
11const pickWindow = (limits: readonly SessionRateLimit[], kind: string): Window | null => {
12  const found = limits.find(l => l.kind === kind)
13  return found ? { percent: found.percentUsed, resetsAt: found.resetsAt } : null
14}
15
16const toUsage = (context: SessionContextUsage, limits: readonly SessionRateLimit[]): Usage => ({
17  context: { percent: context.percent, tokens: context.tokens, window: context.window },
18  fiveHour: pickWindow(limits, 'five_hour'),
19  week: pickWindow(limits, 'seven_day'),
20})
21
22// Uma cor por gráfico; no limite (85% ou mais) qualquer um passa a vermelho.
23const HUES = { ctx: '#4f8ff7', '5h': '#9b7bf0', sem: '#2cb5a0' } as const
24const LIMIT = '#e5534b'
25const TRACK = 'rgba(140,140,140,0.16)'
26const colorFor = (key: keyof typeof HUES, percent: number) => (percent >= 85 ? LIMIT : HUES[key])
27
28// Desktop: cada coluna da faixa mede cerca de 9,5 px (medido: 95 colunas).
29const PX_PER_COLUMN = 9.5
30const BAR_HEIGHT = 8
31const FRAME_HEIGHT = 16
32
33const esc = (text: string) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
34
35// Barra em forma de pílula: calha suave e preenchimento arredondado. Com o rato
36// por cima (CSS :hover, que funciona na moldura isolada do SVG), a pílula dá
37// lugar ao detalhe, no mesmo sítio e na mesma cor.
38const pill = (width: number, percent: number, color: string, title: string, detail: string) => {
39  const r = BAR_HEIGHT / 2
40  const top = (FRAME_HEIGHT - BAR_HEIGHT) / 2
41  const fill = percent <= 0 ? 0 : Math.max(BAR_HEIGHT, (clamp(percent) / 100) * width)
42  return (
43    `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${FRAME_HEIGHT}" viewBox="0 0 ${width} ${FRAME_HEIGHT}">` +
44    `<style>` +
45    `.d{opacity:0;transition:opacity .12s}.b{transition:opacity .12s}` +
46    `svg:hover .b{opacity:0}svg:hover .d{opacity:1}` +
47    `text{font:500 11px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}` +
48    `</style>` +
49    `<title>${esc(title)}</title>` +
50    `<rect width="${width}" height="${FRAME_HEIGHT}" fill="transparent"/>` +
51    `<g class="b">` +
52    `<rect y="${top}" width="${width}" height="${BAR_HEIGHT}" rx="${r}" fill="${TRACK}"/>` +
53    (fill ? `<rect y="${top}" width="${fill.toFixed(1)}" height="${BAR_HEIGHT}" rx="${r}" fill="${color}"/>` : '') +
54    `</g>` +
55    `<text class="d" x="0" y="${FRAME_HEIGHT / 2}" dominant-baseline="central" fill="${color}">${esc(detail)}</text>` +
56    `</svg>`
57  )
58}
59
60const clamp = (percent: number) => Math.min(Math.max(percent, 0), 100)
61
62const kTokens = (n: number) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${Math.round(n / 1000)}k`)
63
64const untilReset = (resetsAt: string | undefined, now: number) => {
65  if (!resetsAt) return ''
66  const ms = Date.parse(resetsAt) - now
67  if (!Number.isFinite(ms) || ms <= 0) return ''
68  const minutes = Math.round(ms / 60_000)
69  const d = Math.floor(minutes / 1440)
70  const h = Math.floor((minutes % 1440) / 60)
71  const m = minutes % 60
72  return `repõe em ${d ? `${d}d ${h}h` : h ? `${h}h ${m}m` : `${m}m`}`
73}
74
75export const register: Register = on => {
76  let current: Usage | null = null
77
78  on('session.start', async ($, e, next) => {
79    const { context, rateLimits } = await $.session.usage()
80    current = toUsage(context, rateLimits)
81    $.ui.invalidate('ui.render')
82    return next(e)
83  })
84
85  on('session.measure', async ($, e, next) => {
86    current = toUsage(e.context, e.rateLimits)
87    $.ui.invalidate('ui.render')
88    return next(e)
89  })
90
91  // Os elementos vêm da tabela de cada superfície. Três colunas de largura igual:
92  // nome, barra e percentagem. No desktop a barra é uma pílula SVG com o detalhe
93  // no hover; no terminal é um bloco de cor e o detalhe entra ao lado no hover.
94  // eslint-disable-next-line @typescript-eslint/no-explicit-any
95  const usageLine = (Box: any, Text: any, Svg: any, usage: Usage, now: number, bodyColumns: number) => {
96    const isTerminal = Svg === undefined
97    const ctx = usage.context
98    const pct = (percent: number | undefined) => (percent === undefined ? '—' : `${Math.round(percent)}%`)
99
100    const columns: { key: keyof typeof HUES; label: string; percent: number | undefined; detail: string }[] = [
101      {
102        key: 'ctx',
103        label: 'Contexto',
104        percent: ctx.percent,
105        detail: ctx.tokens !== undefined ? `${kTokens(ctx.tokens)} / ${kTokens(ctx.window)}` : `${kTokens(ctx.window)} tokens`,
106      },
107      { key: '5h', label: 'Sessão', percent: usage.fiveHour?.percent, detail: untilReset(usage.fiveHour?.resetsAt, now) || 'sem leitura' },
108      { key: 'sem', label: 'Semana', percent: usage.week?.percent, detail: untilReset(usage.week?.resetsAt, now) || 'sem leitura' },
109    ]
110
111    return (
112      <Box flexDirection="row" width="100%" columnGap={4}>
113        {columns.map(c => {
114          const color = c.percent === undefined ? 'inactive' : colorFor(c.key, c.percent)
115          const fill = `${clamp(c.percent ?? 0)}%`
116          const label = `${c.label}: ${pct(c.percent)} · ${c.detail}`
117          // Três colunas, 4 de intervalo; em cada uma, nome (8), percentagem (4) e espaços (2).
118          const barColumns = Math.max(6, Math.floor((bodyColumns - 8) / 3) - 14)
119          const barPx = Math.round(barColumns * PX_PER_COLUMN)
120          const bar = isTerminal ? (
121            <Box flexGrow={1} width={0} height={1} backgroundColor="subtle" overflow="hidden">
122              <Box width={fill} height={1} backgroundColor={color} />
123            </Box>
124          ) : (
125            <Box flexGrow={1} height={1} alignItems="center" overflow="hidden">
126              <Svg
127                source={pill(barPx, c.percent ?? 0, color === 'inactive' ? '#8c8c8c' : color, label, c.detail)}
128                alt={label}
129                width={barPx}
130                height={FRAME_HEIGHT}
131                isInteractive
132              />
133            </Box>
134          )
135          return (
136            <Box key={`usage-${c.key}`} flexGrow={1} width={0} flexDirection="row" alignItems="center" columnGap={1}>
137              <Box width={8} flexShrink={0}>
138                <Text dimColor wrap="truncate-end">{c.label}</Text>
139              </Box>
140              {bar}
141              <Box width={4} flexShrink={0} justifyContent="flex-end">
142                <Text color={color}>{pct(c.percent)}</Text>
143              </Box>
144              {isTerminal ? (
145                <Box display="none" hover={{ display: 'flex' }} flexShrink={0}>
146                  <Text dimColor wrap="truncate-end">{c.detail}</Text>
147                </Box>
148              ) : null}
149            </Box>
150          )
151        })}
152      </Box>
153    )
154  }
155
156  // Uma linha na faixa imediatamente acima da caixa do prompt (terminal e desktop).
157  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
158    if (current === null || e.props.hasSurvey) return next(e)
159
160    const now = await $.clock.now()
161    if (e.surface === 'terminal') {
162      const { Box, Text } = $.ui.resolve(e)
163      return usageLine(Box, Text, undefined, current, now, e.props.bodyColumns)
164    }
165    const { Box, Text, Svg } = $.ui.resolve(e)
166    return usageLine(Box, Text, Svg, current, now, e.props.bodyColumns)
167  })
168}
169