Compact neon usage meters (ctx, 5h, 7d, model, effort, cost) on the permission-mode line.

Mods para o Claude Code: plugins de function hooks que mudam a interface e o comportamento do próprio Claude Code.
| Mod | O que faz |
|---|---|
mods-hub | Botão ◆ mods na linha do mode e janela lateral para ligar, desligar e configurar os mods. |
status-inline | Medidores compactos (contexto, rate limits, modelo, custo, tokens) na linha do mode. |
model-picker | Troca o modelo e o effort da sessão pela janela de mods. |
<img width="1084" height="101" alt="image" src="https://github.com/user-attachments/assets/ac343e6d-1300-4469-9443-7b241f6e7b14" /> <img width="216" height="427" alt="image" src="https://github.com/user-attachments/assets/b03d8f45-afad-45ec-b53d-415e3e89bba8" />
Em qualquer sessão do Claude Code:
/plugin marketplace add Sales16/claude-mods
/plugin install mods-hub@claude-mods
/plugin install status-inline@claude-mods
/plugin install model-picker@claude-mods
mods-hub junto: o status-inline depende dele.Para pegar uma versão nova do repositório:
/plugin marketplace update claude-mods
/reload-plugins
Para remover:
/plugin uninstall model-picker@claude-mods
/plugin uninstall status-inline@claude-mods
/plugin uninstall mods-hub@claude-mods
Útil enquanto você mexe nos mods: o terminal vigia as pastas e recarrega o mod sozinho quando um arquivo muda.
Clone o repositório primeiro. Os exemplos abaixo usam C:\MyProjects\claude-mods; troque pelo caminho do seu clone.
git clone https://github.com/Sales16/claude-mods.git C:\MyProjects\claude-mods
Para todos os terminais, em ~/.claude/settings.json:
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "C:\\MyProjects\\claude-mods\\plugins\\mods-hub;C:\\MyProjects\\claude-mods\\plugins\\status-inline;C:\\MyProjects\\claude-mods\\plugins\\model-picker"
}
}
Para um terminal só:
claude --plugin-dir C:\MyProjects\claude-mods\plugins\mods-hub --plugin-dir C:\MyProjects\claude-mods\plugins\status-inline --plugin-dir C:\MyProjects\claude-mods\plugins\model-picker
Use um jeito só. Instalado e carregado pela pasta ao mesmo tempo, o mod roda duas vezes (dois ◆ mods na linha).
Cada opção dos mods fica salva no settings.json do usuário, em pluginConfigs, e vale entre sessões. O nome da chave muda com o jeito de carregar (status-inline@claude-mods instalado, status-inline@inline pela pasta): ao trocar de jeito, refaça as escolhas pelo ◆ mods.
Adiciona ◆ mods no começo da linha do mode:
◆ mods ⏵⏵ auto mode on │ ctx ▰▰▰▰▰▱▱▱ 62% │ …
Clicar em ◆ mods (ou rodar /mods) abre uma janela lateral com cada mod que tem configurações:
● model-picker
■ Ativo
▸ Modelo: opus
▸ Effort: high
● status-inline
■ Ativo
▾ Barra: ▰▱
● ▰▱
○ █░
■ ctx %
Amarelo a partir de % − 70 +
●/○ liga ou desliga o mod inteiro; ■/□ liga ou desliga cada opção.▸) abre a lista de valores embaixo dela; escolher um aplica na hora.− e +, de 5 em 5.Opção própria: Cor do ◆ mods (verde-neon, ciano, magenta, laranja, azul, roxo, branco).
Clique e hover dependem de o terminal enviar o mouse para o Claude Code, o que acontece no modo tela cheia. Sem mouse, use /mods, Tab e Enter.
Desenha medidores depois da linha do mode, em verde neon:
⏵⏵ auto mode on │ ctx ▰▰▰▰▰▱▱▱ 62% │ 5h ▰▱▱▱▱▱▱▱ 18% ↻14:30 │ 7d ▰▰▰▱▱▱▱▱ 41% ↻sex │ opus5.5·xh │ $1.23 │ tok 1.2M │ cmp ≈18k │ 🔥 →36%
| Medidor | O que mostra |
|---|---|
ctx | Quanto da janela de contexto o último request usou. |
5h | Uso da janela de rate limit de 5 horas e quando ela reseta. |
7d | Uso da janela semanal e o dia em que ela reseta. |
opus5.5·xh | Modelo e effort (lo, md, hi, xh, max) do request atual. |
$1.23 | Custo da sessão, como o /cost soma. |
tok 1.2M | Tokens gastos na sessão, conversa principal e subagents juntos, desde que o mod carregou. |
cmp ≈18k | Tokens que faltam até o auto-compact. |
🔥 →36% | Uso que a janela de 5h terá no reset, se o ritmo atual continuar. |
Configurações:
| Opção | Valores |
|---|---|
| Ativo | liga ou desliga os medidores |
| Cor | verde-neon, ciano, magenta, laranja, azul, roxo, branco |
| Barra | ▰▱, █░, ■□, ●○, ⣿⣀, ━┄ |
| Reset | oculto (18%), valor (18% 9:00), ícone (18% ↻9:00), texto (18% reset às 9:00) |
| Tokens | total, sem-cache (sem o cache relido), in/out (↑1.2M ↓40k), oculto |
| ctx, 5h, 7d, Modelo, Effort, Custo, cmp, Projeção 5h, Barras | mostra ou esconde cada item |
| Amarelo a partir de % | padrão 70 |
| Vermelho a partir de % | padrão 90 |
7d, modelo/effort. ctx e 5h nunca somem.◆ mods aberta ao lado, o mod conta a largura dela, porque a linha do mode passa por baixo das duas.Troca o modelo e o effort da sessão pela janela do ◆ mods.
| Opção | Valores |
|---|---|
| Ativo | liga ou desliga as trocas e a sincronização |
| Modelo | default, opus, sonnet, haiku, fable |
| Effort | low, medium, high, xhigh, max |
/model. Escolher um effort roda /effort <nível>, que executa quando a sessão fica parada./model, /effort ou troca de modelo pelo /config feita à mão passa a aparecer na janela. Um modelo como claude-opus-5-5 ou opus[1m] aparece como opus.claude-mods/
.claude-plugin/marketplace.json lista os mods para o /plugin install
plugins/
mods-hub/ botão ◆ mods, janela lateral, /mods
status-inline/ medidores na linha do mode
model-picker/ modelo e effort pela janela de mods
Cada mod tem .claude-plugin/plugin.json (nome, opções em userConfig), hooks/hooks.json e o módulo em hooks/register.ts(x), com seus testes em hooks/*.test.ts(x).
Um mod aparece na janela quando o plugin.json declara campos em userConfig.
enabled vira o liga/desliga do mod; o mod deve checar options.enabled e ficar inerte quando for false.string com options vira uma lista de valores.description do campo:Explicação do campo, uma ideia por linha.
Ex.: {#39FF14}ctx ▰▰▰▰▰▱▱▱ 62%{/}
-- <opção>
Explicação daquela opção.
Ex.: {#00F0FF}ctx ▰▰▰▰▰▱▱▱ 62%{/}
O Claude Code remove as quebras de linha do description, então o hub reencontra as linhas: uma linha começa em -- <opção> (uma das opções do campo), em Ex.: e depois de ., ! ou ? quando a frase seguinte começa com letra maiúscula. Comece toda linha assim e termine cada frase com ponto. Uma linha Ex.: é desenhada como exemplo, e {#RRGGBB}texto{/} pinta aquele trecho na cor.
No /config do Claude Code a mesma descrição aparece como texto puro, com as marcações cruas.
claude plugin validate plugins/<mod>
claude plugin test plugins/<mod>hooks/register.tsx 162 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionMeasureInput } from 'claude-code'
3
4import type { InlineSnapshot, TokenTally } from '../types'
5import { layout, readLimits, readShow, readTheme, textWidth } from './format'
6
7const snap = atom({ plugin: 'status-inline', key: 'snap' } as const, {})
8
9/** Columns the mods-hub pane takes from the transcript while docked beside it. */
10const hubDock = { plugin: 'mods-hub', key: 'dock' } as const
11
12const NO_TOKENS: TokenTally = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
13
14/** Columns kept free for the mods-hub `◆ mods` button and its space. */
15const HUB_RESERVE = 7
16const TICK_MS = 60 * 1000
17
18type Usage = Pick<SessionMeasureInput, 'context' | 'rateLimits' | 'cost'>
19
20function fromUsage(usage: Usage): Partial<InlineSnapshot> {
21 const window = (kind: string) => {
22 const found = usage.rateLimits.find(limit => limit.kind === kind)
23
24 return found === undefined ? undefined : { percent: found.percentUsed, resetsAt: found.resetsAt }
25 }
26
27 return {
28 ctxPercent: usage.context.percent,
29 ctxTokens: usage.context.tokens,
30 fiveHour: window('five_hour'),
31 sevenDay: window('seven_day'),
32 costUsd: usage.cost?.usd,
33 }
34}
35
36async function readCompactAt($: EngineInterface): Promise<number | undefined> {
37 try {
38 const { context } = await $.session.usage({ breakdown: 'summary' })
39
40 return context.breakdown?.autoCompactThreshold
41 } catch {
42 // The breakdown needs a bound session; the meters draw without cmp.
43 return undefined
44 }
45}
46
47export const register: Register = (on, options) => {
48 const show = readShow(options)
49 const limits = readLimits(options)
50 const theme = readTheme(options)
51
52 on('session.start', async ($, e, next) => {
53 const started = await next(e)
54 // The first turn.step reports the effort in force; until then the
55 // process environment and the settings hold the session's choice.
56 const [sessionId, usage, model, settings, envEffort, compactAt, now] = await Promise.all([
57 $.session.id(),
58 $.session.usage(),
59 $.session.model(),
60 $.settings.read(),
61 $.env.get('CLAUDE_EFFORT'),
62 readCompactAt($),
63 $.clock.now(),
64 ])
65 const settingsEffort = (settings as Record<string, unknown>)['effortLevel']
66 const effort = envEffort ?? (typeof settingsEffort === 'string' ? settingsEffort : undefined)
67 await update($, snap, old => ({
68 ...old,
69 ...fromUsage(usage),
70 model,
71 effort: effort ?? old.effort,
72 compactAt: compactAt ?? old.compactAt,
73 now,
74 sessionId,
75 tokens: old.sessionId === sessionId ? old.tokens : undefined,
76 }))
77 $.clock.every(TICK_MS, () => {
78 void $.clock.now().then(now => update($, snap, old => ({ ...old, now })))
79 })
80
81 return started
82 })
83
84 on('session.measure', async ($, e, next) => {
85 const now = await $.clock.now()
86 const old = await read($, snap)
87 const compactAt = old.compactAt ?? (await readCompactAt($))
88 await update($, snap, current => ({ ...current, ...fromUsage(e), compactAt, now }))
89
90 return next(e)
91 })
92
93 on('session.end', { reason: 'clear' }, async ($, e, next) => {
94 await update($, snap, old => ({ ...old, tokens: undefined }))
95
96 return next(e)
97 })
98
99 on('turn.step', async function* ($, e, next) {
100 if (e.agentId === undefined) {
101 const effort = e.effort === undefined ? undefined : String(e.effort)
102 const old = await read($, snap)
103 const hasModelChanged = old.model !== undefined && old.model !== e.model
104 await update($, snap, current => ({
105 ...current,
106 model: e.model,
107 effort: effort ?? current.effort,
108 compactAt: hasModelChanged ? undefined : current.compactAt,
109 }))
110 }
111
112 const result = yield* next(e)
113 const usage = result.usage
114 if (usage !== null) {
115 await update($, snap, current => {
116 const tally = current.tokens ?? NO_TOKENS
117
118 return {
119 ...current,
120 tokens: {
121 input: tally.input + usage.input_tokens,
122 output: tally.output + usage.output_tokens,
123 cacheRead: tally.cacheRead + usage.cache_read_input_tokens,
124 cacheWrite: tally.cacheWrite + usage.cache_creation_input_tokens,
125 },
126 }
127 })
128 }
129
130 return result
131 })
132
133 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
134 const drawn = await next(e)
135 if (options['enabled'] === false) return drawn
136
137 const current = await read($, snap)
138 // The mode line runs under the transcript and a docked pane alike, while
139 // the viewport counts the transcript's columns alone.
140 const { value: dock = 0 } = await $.state.get(hubDock)
141 const columns = (e.viewport?.columns ?? 100) + dock
142 const used = textWidth(e.props.hint) + textWidth(e.props.tail ?? '') + HUB_RESERVE
143 const parts = layout(current, show, limits, theme, columns - used - 1, current.now ?? 0)
144 if (parts.length === 0) return drawn
145
146 const { Box, Text } = $.ui.resolve(e)
147
148 return (
149 <Box flexDirection="row">
150 {drawn}
151 <Text wrap="truncate-end">
152 {parts.map(part => (
153 <Text color={part.color} dimColor={part.isDim === true}>
154 {part.text}
155 </Text>
156 ))}
157 </Text>
158 </Box>
159 )
160 })
161}
162hooks/format.ts 283 lines1import type { InlineSnapshot, TokenTally, UsageWindow } from '../types'
2
3export const NEON = '#39FF14'
4export const YELLOW = '#FFE600'
5export const RED = '#FF3131'
6
7/** The `color` option's picks, by the name `/config` and the mods pane show. */
8export const COLORS: Record<string, string> = {
9 'verde-neon': NEON,
10 ciano: '#00F0FF',
11 magenta: '#FF2BD6',
12 laranja: '#FF9F1C',
13 azul: '#3D8BFF',
14 roxo: '#B266FF',
15 branco: '#F2F2F2',
16}
17
18/** The `bar_style` option's picks: the filled cell, then the empty one. */
19export const BAR_STYLES = ['▰▱', '█░', '■□', '●○', '⣿⣀', '━┄']
20
21/** How a reset time is labelled: hidden, `9:00`, `↻9:00` or `reset às 9:00`. */
22export type ResetLabel = 'oculto' | 'valor' | 'ícone' | 'texto'
23
24const RESET_LABELS: readonly ResetLabel[] = ['oculto', 'valor', 'ícone', 'texto']
25
26const BAR_CELLS = 8
27
28/** How the session's tokens show: hidden, every token, without cache reads, or in and out apart. */
29export type TokensMode = 'oculto' | 'total' | 'sem-cache' | 'in/out'
30
31const TOKENS_MODES: readonly TokensMode[] = ['oculto', 'total', 'sem-cache', 'in/out']
32
33export type Part = { text: string; color: string; isDim?: boolean }
34
35export type Show = {
36 ctx: boolean
37 fiveHour: boolean
38 sevenDay: boolean
39 model: boolean
40 effort: boolean
41 cost: boolean
42 compact: boolean
43 burn: boolean
44 bars: boolean
45 tokens: TokensMode
46}
47
48export type Limits = { warnAt: number; dangerAt: number }
49
50export type Theme = { accent: string; full: string; empty: string; resetLabel: ResetLabel }
51
52export const DEFAULT_THEME: Theme = { accent: NEON, full: '▰', empty: '▱', resetLabel: 'ícone' }
53
54const FIVE_HOURS_MS = 5 * 60 * 60 * 1000
55const MIN_ELAPSED_FOR_PROJECTION_MS = 10 * 60 * 1000
56const WEEKDAYS = ['dom', 'seg', 'ter', 'qua', 'qui', 'sex', 'sáb']
57const EFFORTS: Record<string, string> = { low: 'lo', medium: 'md', high: 'hi', xhigh: 'xh', max: 'max' }
58
59/** Drop order for narrow terminals: each level removes one more group. */
60const LEVELS = 6
61
62export function readShow(options: Readonly<Record<string, unknown>>): Show {
63 const on = (key: string) => options[key] !== false
64
65 return {
66 ctx: on('ctx'),
67 fiveHour: on('five_hour'),
68 sevenDay: on('seven_day'),
69 model: on('model'),
70 effort: on('effort'),
71 cost: on('cost'),
72 compact: on('compact'),
73 burn: on('burn'),
74 bars: on('bars'),
75 tokens: TOKENS_MODES.find(mode => mode === options['tokens']) ?? 'total',
76 }
77}
78
79export function readLimits(options: Readonly<Record<string, unknown>>): Limits {
80 const num = (key: string, fallback: number) =>
81 typeof options[key] === 'number' ? (options[key] as number) : fallback
82
83 return { warnAt: num('warn_at', 70), dangerAt: num('danger_at', 90) }
84}
85
86export function readTheme(options: Readonly<Record<string, unknown>>): Theme {
87 const accent = COLORS[String(options['color'])] ?? NEON
88 const style = BAR_STYLES.includes(String(options['bar_style'])) ? String(options['bar_style']) : '▰▱'
89 const [full = '▰', empty = '▱'] = Array.from(style)
90 const resetLabel = RESET_LABELS.find(label => label === options['reset_label']) ?? DEFAULT_THEME.resetLabel
91
92 return { accent, full, empty, resetLabel }
93}
94
95export function levelColor(percent: number, limits: Limits, theme: Theme = DEFAULT_THEME): string {
96 if (percent >= limits.dangerAt) return RED
97 if (percent >= limits.warnAt) return YELLOW
98
99 return theme.accent
100}
101
102export function bar(percent: number, theme: Theme = DEFAULT_THEME): string {
103 const filled = Math.max(0, Math.min(BAR_CELLS, Math.round((percent / 100) * BAR_CELLS)))
104
105 return theme.full.repeat(filled) + theme.empty.repeat(BAR_CELLS - filled)
106}
107
108export function shortModel(model: string): string {
109 return model
110 .toLowerCase()
111 .replace(/\(.*?\)/g, '')
112 .replace(/-\d{8}$/, '')
113 .replace(/^claude[-\s]/, '')
114 .trim()
115 .replace(/(\d+)[-.](\d+)/g, '$1.$2')
116 .replace(/[\s-]+/g, '')
117}
118
119export function shortEffort(effort: string): string {
120 return EFFORTS[effort] ?? effort
121}
122
123export function resetClock(iso: string): string {
124 const at = new Date(iso)
125
126 return `${at.getHours()}:${String(at.getMinutes()).padStart(2, '0')}`
127}
128
129export function resetDay(iso: string): string {
130 return WEEKDAYS[new Date(iso).getDay()] ?? ''
131}
132
133/** A reset as the `reset_label` option spells it; undefined when hidden. */
134export function resetText(value: string, preposition: string, label: ResetLabel): string | undefined {
135 if (label === 'oculto') return undefined
136 if (label === 'valor') return value
137 if (label === 'texto') return `reset ${preposition} ${value}`
138
139 return `↻${value}`
140}
141
142export function kTokens(tokens: number): string {
143 if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`
144
145 return tokens >= 1000 ? `${Math.round(tokens / 1000)}k` : String(tokens)
146}
147
148export function tokensText(tally: TokenTally, mode: TokensMode): string | undefined {
149 const input = tally.input + tally.cacheWrite
150 if (mode === 'oculto') return undefined
151 if (mode === 'in/out') return `tok ↑${kTokens(input + tally.cacheRead)} ↓${kTokens(tally.output)}`
152 if (mode === 'sem-cache') return `tok ${kTokens(input + tally.output)}`
153
154 return `tok ${kTokens(input + tally.cacheRead + tally.output)}`
155}
156
157/** Percent of the 5h window used at reset time if the current pace holds. */
158export function projectFiveHour(window: UsageWindow, now: number): number | undefined {
159 if (window.resetsAt === undefined) return undefined
160 const startedAt = new Date(window.resetsAt).getTime() - FIVE_HOURS_MS
161 const elapsed = now - startedAt
162 if (elapsed < MIN_ELAPSED_FOR_PROJECTION_MS || elapsed > FIVE_HOURS_MS) return undefined
163
164 return Math.round((window.percent * FIVE_HOURS_MS) / elapsed)
165}
166
167export function textWidth(text: string): number {
168 let width = 0
169 for (const char of text) width += (char.codePointAt(0) ?? 0) >= 0x1f000 ? 2 : 1
170
171 return width
172}
173
174function meter(label: string, percent: number, limits: Limits, theme: Theme, hasBar: boolean): Part[] {
175 const color = levelColor(percent, limits, theme)
176 const parts: Part[] = [{ text: `${label} `, color: theme.accent }]
177 if (hasBar) parts.push({ text: `${bar(percent, theme)} `, color })
178 parts.push({ text: `${Math.round(percent)}%`, color })
179
180 return parts
181}
182
183function segmentsAt(
184 level: number,
185 snap: InlineSnapshot,
186 show: Show,
187 limits: Limits,
188 theme: Theme,
189 now: number,
190): Part[][] {
191 const hasBars = show.bars && level < 1
192 const hasExtras = level < 2
193 const hasResets = level < 3
194 const hasSevenDay = level < 4
195 const hasModel = level < 5
196 const segments: Part[][] = []
197
198 if (show.ctx && snap.ctxPercent !== undefined) {
199 segments.push(meter('ctx', snap.ctxPercent, limits, theme, hasBars))
200 }
201
202 if (show.fiveHour && snap.fiveHour !== undefined) {
203 const parts = meter('5h', snap.fiveHour.percent, limits, theme, hasBars)
204 const reset =
205 snap.fiveHour.resetsAt === undefined
206 ? undefined
207 : resetText(resetClock(snap.fiveHour.resetsAt), 'às', theme.resetLabel)
208 if (hasResets && reset !== undefined) parts.push({ text: ` ${reset}`, color: theme.accent, isDim: true })
209 segments.push(parts)
210 }
211
212 if (show.sevenDay && hasSevenDay && snap.sevenDay !== undefined) {
213 const parts = meter('7d', snap.sevenDay.percent, limits, theme, hasBars)
214 const reset =
215 snap.sevenDay.resetsAt === undefined
216 ? undefined
217 : resetText(resetDay(snap.sevenDay.resetsAt), 'em', theme.resetLabel)
218 if (hasResets && reset !== undefined) parts.push({ text: ` ${reset}`, color: theme.accent, isDim: true })
219 segments.push(parts)
220 }
221
222 if (hasModel) {
223 const names = [
224 show.model && snap.model !== undefined ? shortModel(snap.model) : undefined,
225 show.effort && snap.effort !== undefined ? shortEffort(snap.effort) : undefined,
226 ].filter((name): name is string => name !== undefined && name !== '')
227 if (names.length > 0) segments.push([{ text: names.join('·'), color: theme.accent }])
228 }
229
230 if (hasExtras) {
231 if (show.cost && snap.costUsd !== undefined) {
232 segments.push([{ text: `$${snap.costUsd.toFixed(2)}`, color: theme.accent }])
233 }
234
235 const tokens = snap.tokens === undefined ? undefined : tokensText(snap.tokens, show.tokens)
236 if (tokens !== undefined) segments.push([{ text: tokens, color: theme.accent }])
237
238 if (show.compact && snap.compactAt !== undefined && snap.ctxTokens !== undefined) {
239 const left = Math.max(0, snap.compactAt - snap.ctxTokens)
240 const color = left < snap.compactAt * 0.1 ? YELLOW : theme.accent
241 segments.push([{ text: `cmp ≈${kTokens(left)}`, color }])
242 }
243
244 const projected = show.burn && snap.fiveHour !== undefined ? projectFiveHour(snap.fiveHour, now) : undefined
245 if (projected !== undefined) {
246 segments.push([{ text: `🔥 →${projected}%`, color: levelColor(projected, limits, theme) }])
247 }
248 }
249
250 return segments
251}
252
253function join(segments: Part[][], theme: Theme): Part[] {
254 const separator: Part = { text: ' │ ', color: theme.accent, isDim: true }
255
256 return segments.flatMap(segment => [separator, ...segment])
257}
258
259function width(parts: Part[]): number {
260 return parts.reduce((sum, part) => sum + textWidth(part.text), 0)
261}
262
263/**
264 * The parts to draw after the mode line, each led by a separator: the richest
265 * level that fits in `room` columns, else the leanest (the line truncates it).
266 */
267export function layout(
268 snap: InlineSnapshot,
269 show: Show,
270 limits: Limits,
271 theme: Theme,
272 room: number,
273 now: number,
274): Part[] {
275 let parts: Part[] = []
276 for (let level = 0; level < LEVELS; level++) {
277 parts = join(segmentsAt(level, snap, show, limits, theme, now), theme)
278 if (width(parts) <= room) return parts
279 }
280
281 return parts
282}
283types/index.d.ts 30 lines1export type UsageWindow = { percent: number; resetsAt?: string }
2
3/** Tokens the session's model requests reported, main loop and subagents together. */
4export type TokenTally = { input: number; output: number; cacheRead: number; cacheWrite: number }
5
6export type InlineSnapshot = {
7 /** Context window fill, 0-100. */
8 ctxPercent?: number
9 /** Input tokens of the last response. */
10 ctxTokens?: number
11 /** Token count at which auto-compaction runs. */
12 compactAt?: number
13 fiveHour?: UsageWindow
14 sevenDay?: UsageWindow
15 model?: string
16 effort?: string
17 costUsd?: number
18 /** Last clock tick, ms since epoch; drives reset and projection redraws. */
19 now?: number
20 /** The session the tally counts, so a new session starts it over. */
21 sessionId?: string
22 tokens?: TokenTally
23}
24
25declare module 'claude-code' {
26 interface PluginState {
27 'status-inline': { snap: InlineSnapshot }
28 }
29}
30