SLOPSHOPPER

uso-sessao

Mostra contexto, custo e uso da assinatura na status line

newband
v0.1.0no licenseupdated 2026-10-06diogosarti/my-claude-code/plugins/uso-sessao
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · uso-sessao
› 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 ctx 97k/200k (49%) · $0.42 · 5h: 69% livre ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
ctx 97k/200k (49%) · $0.42 · 5h: 69% livre
README

my-claude-code

Meus mods para o Claude Code.

uso-sessao

Uma linha acima do prompt com o contexto da sessão, o custo e quanto sobra dos limites da assinatura:

ctx 50k/200k (25%) · $1.23 · 5h: 40% livre (reset 1h20) · 7d: 82% livre (reset 3d4h)
  • Contexto: verde abaixo de 50%, amarelo até 80%, vermelho acima.
  • Limites 5h e 7d: a cor vem do ritmo. O mod projeta quanto você terá usado no reset, no ritmo atual: verde abaixo de 85%, amarelo até 100%, vermelho se for estourar antes do reset (ou acima de 95% já usado).
  • Custo: o mesmo valor do /cost. Numa assinatura é o equivalente em API, não o que você paga.

Instalar

/plugin install uso-sessao --marketplace diogosarti/my-claude-code

Responda y para adicionar o marketplace e escolha o escopo de usuário.

Source 2 files
hooks/register.tsx 99 lines
1import { atom, read, update } from 'claude-code'
2import type { Register, SessionRateLimit } from 'claude-code'
3
4import type { Cor, Parte } from '../types'
5
6const NOMES: Record<string, string> = { five_hour: '5h', seven_day: '7d' }
7
8const DURACAO_MS: Record<string, number> = { five_hour: 5 * 3600_000, seven_day: 7 * 86400_000 }
9
10export function corDoUso(percentUsado: number): Cor {
11  if (percentUsado >= 80) return 'error'
12  if (percentUsado >= 50) return 'warning'
13  return 'success'
14}
15
16// Projeta o uso no fim da janela pelo ritmo atual. O decorrido tem piso de 10%
17// para o começo da janela não projetar estouro com 1% usado.
18export function corDoRitmo(r: SessionRateLimit, agora: number): Cor {
19  const duracao = DURACAO_MS[r.kind]
20  const reset = r.resetsAt ? Date.parse(r.resetsAt) : Number.NaN
21  if (r.percentUsed >= 95) return 'error'
22  if (!duracao || Number.isNaN(reset)) return corDoUso(r.percentUsed)
23  const decorrido = Math.min(1, Math.max(0.1, 1 - (reset - agora) / duracao))
24  const projetado = r.percentUsed / decorrido
25  if (projetado >= 100) return 'error'
26  if (projetado >= 85) return 'warning'
27  return 'success'
28}
29
30export function faltaPara(resetsAt: string | undefined, agora: number): string {
31  const ms = resetsAt ? Date.parse(resetsAt) - agora : Number.NaN
32  if (Number.isNaN(ms) || ms <= 0) return ''
33  const min = Math.round(ms / 60_000)
34  if (min < 60) return ` (reset ${min}min)`
35  const h = Math.floor(min / 60)
36  if (h < 24) return ` (reset ${h}h${String(min % 60).padStart(2, '0')})`
37  return ` (reset ${Math.floor(h / 24)}d${h % 24}h)`
38}
39
40export function formatar(
41  context: { tokens?: number; window: number; percent?: number },
42  rateLimits: SessionRateLimit[],
43  usd: number | undefined,
44  agora: number,
45): Parte[] {
46  const k = (n: number) => `${Math.round(n / 1000)}k`
47  const partes: Parte[] = [
48    {
49      texto:
50        `ctx ${context.tokens === undefined ? '-' : k(context.tokens)}/${k(context.window)}` +
51        (context.percent === undefined ? '' : ` (${Math.round(context.percent)}%)`),
52      cor: context.percent === undefined ? 'subtle' : corDoUso(context.percent),
53    },
54  ]
55  if (usd !== undefined) partes.push({ texto: `$${usd.toFixed(2)}`, cor: 'subtle' })
56  for (const r of rateLimits) {
57    const resta = Math.max(0, 100 - Math.round(r.percentUsed))
58    partes.push({
59      texto: `${NOMES[r.kind] ?? r.kind}: ${resta}% livre${faltaPara(r.resetsAt, agora)}`,
60      cor: corDoRitmo(r, agora),
61    })
62  }
63  return partes
64}
65
66const linha = atom({ plugin: 'uso-sessao', key: 'linha' } as const, null)
67
68export const register: Register = on => {
69  on('session.start', async ($, e, next) => {
70    const u = await $.session.usage()
71    const agora = await $.clock.now()
72    await update($, linha, () => formatar(u.context, u.rateLimits, u.cost?.usd, agora))
73    return next(e)
74  })
75
76  on('session.measure', async ($, e, next) => {
77    const u = await $.session.usage()
78    const agora = await $.clock.now()
79    await update($, linha, () => formatar(e.context, e.rateLimits, u.cost?.usd, agora))
80    return next(e)
81  })
82
83  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
84    const partes = await read($, linha)
85    if (partes === null || e.props.hasSurvey) return next(e)
86    const { Text } = $.ui.resolve(e)
87    return (
88      <Text>
89        {partes.map((p, i) => (
90          <Text key={String(i)} color={p.cor}>
91            {i > 0 ? ' · ' : ''}
92            {p.texto}
93          </Text>
94        ))}
95      </Text>
96    )
97  })
98}
99
types/index.d.ts 10 lines
1export type Cor = 'success' | 'warning' | 'error' | 'subtle'
2export type Parte = { texto: string; cor: Cor }
3export type Linha = Parte[] | null
4
5declare module 'claude-code' {
6  interface PluginState {
7    'uso-sessao': { linha: Linha }
8  }
9}
10