SLOPSHOPPER

status-inline

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

newspinnertimer
v0.1.0no licenseupdated 2026-10-14Sales16/claude-mods/plugins/status-inline
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · status-inline
› 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ⟨Claude Code's own drawing⟩ │ ctx ▰▰▰▰▱▱▱▱ 49% │ 5h ▰▰▱▱▱▱▱▱ 31% ↻9:53 │ opus5.5 │ $0.42 │ 🔥 →39%

Draws

Prompt hint
⟨Claude Code's own drawing⟩ │ ctx ▰▰▰▰▱▱▱▱ 49% │ 5h ▰▰▱▱▱▱▱▱ 31% ↻9:53 │ opus5.5 │ $0.42 │ 🔥 →39%
README

claude-mods

Mods para o Claude Code: plugins de function hooks que mudam a interface e o comportamento do próprio Claude Code.

ModO que faz
mods-hubBotão ◆ mods na linha do mode e janela lateral para ligar, desligar e configurar os mods.
status-inlineMedidores compactos (contexto, rate limits, modelo, custo, tokens) na linha do mode.
model-pickerTroca 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" />

Instalar

Em todos os terminais (recomendado)

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
  • Escolha o escopo user na instalação: os mods passam a carregar em todo terminal novo, em qualquer projeto.
  • Instale o 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

Direto das pastas, sem instalar

Ú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).

Configurações

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.

mods-hub

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.
  • Uma opção com vários valores (▸) abre a lista de valores embaixo dela; escolher um aplica na hora.
  • Números têm − e +, de 5 em 5.
  • Passar o mouse numa opção mostra, no rodapé da janela, o que ela faz e um exemplo já na cor e no estilo daquela escolha.
  • Toda mudança vale na hora: o mod mexido recarrega com a opção nova.

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.

status-inline

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%
MedidorO que mostra
ctxQuanto da janela de contexto o último request usou.
5hUso da janela de rate limit de 5 horas e quando ela reseta.
7dUso da janela semanal e o dia em que ela reseta.
opus5.5·xhModelo e effort (lo, md, hi, xh, max) do request atual.
$1.23Custo da sessão, como o /cost soma.
tok 1.2MTokens gastos na sessão, conversa principal e subagents juntos, desde que o mod carregou.
cmp ≈18kTokens que faltam até o auto-compact.
🔥 →36%Uso que a janela de 5h terá no reset, se o ritmo atual continuar.

Configurações:

OpçãoValores
Ativoliga ou desliga os medidores
Corverde-neon, ciano, magenta, laranja, azul, roxo, branco
Barra▰▱, █░, ■□, ●○, ⣿⣀, ━┄
Resetoculto (18%), valor (18% 9:00), ícone (18% ↻9:00), texto (18% reset às 9:00)
Tokenstotal, sem-cache (sem o cache relido), in/out (↑1.2M ↓40k), oculto
ctx, 5h, 7d, Modelo, Effort, Custo, cmp, Projeção 5h, Barrasmostra ou esconde cada item
Amarelo a partir de %padrão 70
Vermelho a partir de %padrão 90
  • Em tela estreita os itens somem nesta ordem: barras, extras (custo, tokens, cmp, projeção), horários de reset, 7d, modelo/effort. ctx e 5h nunca somem.
  • Com a janela do ◆ mods aberta ao lado, o mod conta a largura dela, porque a linha do mode passa por baixo das duas.

model-picker

Troca o modelo e o effort da sessão pela janela do ◆ mods.

OpçãoValores
Ativoliga ou desliga as trocas e a sincronização
Modelodefault, opus, sonnet, haiku, fable
Effortlow, medium, high, xhigh, max
  • Escolher um modelo troca na hora, como o /model. Escolher um effort roda /effort <nível>, que executa quando a sessão fica parada.
  • Na primeira vez que carrega, o mod adota o modelo e o effort atuais da sessão: instalar não troca nada.
  • Um /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.
  • Trocar de modelo não perde a conversa, mas o primeiro request depois da troca relê o contexto sem cache: sai mais caro e mais lento.

Estrutura

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).

Criar um mod que aparece no ◆ mods

Um mod aparece na janela quando o plugin.json declara campos em userConfig.

  • Um campo booleano chamado enabled vira o liga/desliga do mod; o mod deve checar options.enabled e ficar inerte quando for false.
  • Um campo string com options vira uma lista de valores.
  • A ajuda do hover vem do 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.

Desenvolver

claude plugin validate plugins/<mod>
claude plugin test plugins/<mod>
Source 3 files
hooks/register.tsx 162 lines
1import { 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}
162
hooks/format.ts 283 lines
1import 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}
283
types/index.d.ts 30 lines
1export 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