Avisa antes de um envio que regrava o cache frio (parado além do ttl, contexto acima de 100k) e mostra contexto e tempo de cache em /cache-frio.

Cinco mods para o Claude Code, escritos como plugins de function hooks. Desenham barras e faixas acima do prompt, avisam sobre cache e uso do plano, sobem um servidor de prévia para HTML e lembram o modelo de usar o seu design system.
Funcionam no terminal e na aba Code do app desktop. Textos e comandos estão em português.

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 no bloco env do ~/.claude/settings.json. Sem ela nenhum dos mods carrega.{
"env": {
"CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1"
}
}
Se o arquivo já tem um bloco env, acrescente só a linha da variável.
Num terminal com claude aberto, instale cada mod que quiser:
/plugin install progresso --marketplace Kelvi-Maycon/claude-code-mods
/plugin install barra-uso --marketplace Kelvi-Maycon/claude-code-mods
/plugin install cache-frio --marketplace Kelvi-Maycon/claude-code-mods
/plugin install previa --marketplace Kelvi-Maycon/claude-code-mods
/plugin install ds-primeiro --marketplace Kelvi-Maycon/claude-code-mods
Na primeira vez o Claude Code pergunta se quer adicionar o marketplace: responda y. Depois escolha o escopo do usuário (o primeiro da lista). O mod já roda na sessão atual e nas próximas.
Instalado no escopo do usuário pelo terminal, o mod carrega também na aba Code do app desktop. O comando /plugin install só existe no terminal.

Barras de progresso acima do prompt. O modelo recebe uma ferramenta (progresso) e uma regra curta no system prompt: em tarefa com vários passos, cria uma barra com o plano e a atualiza conforme avança. Cada barra mostra o passo ativo, a etapa, o percentual e o estado (andamento, esperando você, falhou, concluída).
Comandos:
/progresso: mostra ou esconde as barras. Aceita on, off ou painel (abre o painel lateral)./progresso-demo: demonstração de 30 segundos com todos os estados, faixas, painel e sons./progresso-limpar: remove todas as barras e faixas.Ícones por tipo de agente: executor-leve, executor, executor-pesado, executor-design, investigador e leitor têm robôs próprios. Explore usa a lupa do investigador. general-purpose, Plan, claude, fork e qualquer outro nome usam o robô padrão, com o nome do tipo em cinza. Modelo e esforço vêm do frontmatter (name, model, effort) dos arquivos em ~/.claude/agents/.
Um passo cujo título termina com o tipo do agente entre parênteses, como Revisar tudo (executor-pesado), aparece no painel como agente planejado.
![]()
Uma linha de pílulas acima do campo:
5H e 7D: uso do plano nas janelas de 5 horas e 7 dias, com o tempo até renovar.CTX: contexto usado até a compactação automática, pelo limite que o Claude Code informa (275k quando ele não informa nenhum).CACHE: tempo restante do cache de prompt (TTL de 60 minutos, fixo) e taxa de acerto.DS on/off: só aparece com o ds-primeiro configurado. Clicar alterna o DS nesta sessão.Comando: /uso, com on ou off.
Quando a sessão ficou parada além do TTL do cache e o contexto passa de 100k tokens, o próximo envio regrava o cache inteiro e custa mais. O mod segura esse envio uma vez, devolve o texto ao campo e avisa. Enviar de novo dentro de 3 minutos passa direto. Comandos de barra (/compact, /clear) nunca são segurados.
Comando: /cache-frio mostra contexto e tempo de cache. /cache-frio ttl <minutos> ajusta o TTL (padrão 60), /cache-frio off e /cache-frio on desligam e ligam o aviso.
Quando o modelo grava ou edita um .html, o mod sobe um python3 -m http.server na pasta do arquivo, a partir da porta 8765, e mostra a URL acima do prompt com botões para abrir, copiar e parar. O modelo recebe uma linha dizendo que o servidor já está no ar, para não abrir outro. Comandos Bash que matariam o servidor (kill, pkill, killall no processo dele) são negados.
Arquivos em /tmp e /var/folders não viram prévia.
Comando: /previa <caminho> serve uma pasta, /previa status lista os servidores, /previa parar encerra.
Requisitos: python3 e lsof no PATH. O botão Abrir usa o comando open do macOS; em Linux ele não faz nada e o botão de copiar continua funcionando.
No primeiro pedido de peça visual da sessão (página, landing, dashboard, slide, tela, componente...), anexa ao contexto uma linha pedindo que o modelo carregue a skill do seu design system antes de criar. Antes do envio aparece uma faixa avisando, com o botão "Tirar deste envio". A detecção é por verbos e alvos em português.
Não anexa quando o pedido já cita um design system, uma identidade visual, a sua skill, o rótulo ou um dos termos extras. Depois de compactar ou de /clear, volta a anexar no próximo pedido visual.
Sem configuração o mod não faz nada: não anexa, não mostra faixa e não registra comando. Para ligar, configure as opções do plugin. A tela de opções aparece na instalação, e depois fica em /config:
| Opção | O que é | Exemplo |
|---|---|---|
skill | Nome da skill do design system. Vazio desliga o mod. | meu-design-system |
rotulo | Nome exibido na faixa, no toast e na linha anexada. Vazio usa o nome da skill. | Minha Marca |
ignorar | Termos extras, separados por vírgula, que dispensam a linha quando citados. | marca do cliente, sem ds |
As opções ficam salvas no ~/.claude/settings.json, em pluginConfigs.
Comando: /ds mostra o estado. /ds on e /ds off valem para a sessão; /ds sempre on e /ds sempre off mudam o padrão.
claude plugin update <mod>@claude-code-mods
Depois, numa sessão aberta, rode /reload-plugins.
claude plugin uninstall <mod>@claude-code-mods
Cada plugin fica em plugins/<nome>/, com os hooks em hooks/ e os testes em tests/.
claude plugin validate plugins/progresso
claude plugin test plugins/progresso
O tsconfig.json de cada plugin estende .claude-plugin/types/tsconfig.json, que o Claude Code gera no primeiro carregamento do plugin. Num clone novo o editor e o tsc acusam tipos ausentes até esse primeiro carregamento (ou um claude plugin validate). A pasta gerada fica fora do git.
Os sons do progresso são gerados por plugins/progresso/sounds/gerar.py.
A estrutura das barras do progresso foi portada do plan-progress (zycck/claude-mods), de Kirill Serditov, sob licença MIT. O texto da licença está em plugins/progresso/licenses/plan-progress-LICENSE.
MIT. Veja LICENSE.
hooks/register.tsx 226 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Sessao } from '../types'
5
6const NOVA: Sessao = { fim: 0, ctx: 0, hit: null, rodando: false, avisadoEm: 0, liberado: false }
7const sessao = atom({ plugin: 'cache-frio', key: 'sessao' } as const, NOVA)
8
9const TTL_PADRAO_MIN = 60
10const CTX_MINIMO = 100_000
11const JANELA_SEGUNDO_ENVIO_MS = 3 * 60_000
12const MINUTO = 60_000
13// Fim do último turno, ctx e hit por id de sessão, para a retomada depois de
14// relançar o app; `$.clock.now()` é ms desde a época, comparável entre processos.
15const CHAVE_SESSAO = 'sessao:'
16const MAX_SESSOES = 30
17
18type Guardada = Pick<Sessao, 'fim' | 'ctx' | 'hit'>
19
20const k = (tokens: number) => `${Math.round(tokens / 1000)}k`
21
22const duracao = (ms: number) => {
23 const min = Math.floor(ms / MINUTO)
24
25 return min < 60 ? `${min} min` : `${Math.floor(min / 60)}h${String(min % 60).padStart(2, '0')}`
26}
27
28let ttlMin = TTL_PADRAO_MIN
29let avisa = true
30
31const cache = (s: Sessao, agora: number) => {
32 const resta = s.rodando ? ttlMin * MINUTO : s.fim + ttlMin * MINUTO - agora
33
34 return resta > 0 ? `cache ${Math.ceil(resta / MINUTO)}min` : 'cache frio'
35}
36
37async function linha($: EngineInterface) {
38 const s = await read($, sessao)
39
40 if (s.fim === 0) {
41 return undefined
42 }
43
44 const texto = cache(s, await $.clock.now())
45 const comCtx = s.ctx > 0 ? `ctx ${k(s.ctx)} · ${texto}` : texto
46
47 return s.hit != null ? `${comCtx} · ${s.hit}% hit` : comCtx
48}
49
50async function guarda($: EngineInterface, s: Sessao) {
51 const feita: Guardada = { fim: s.fim, ctx: s.ctx, hit: s.hit ?? null }
52 await $.store.set(CHAVE_SESSAO + (await $.session.id()), feita)
53}
54
55async function restaura($: EngineInterface) {
56 const feita = (await $.store.get(CHAVE_SESSAO + (await $.session.id()))) as Guardada | undefined
57
58 if (feita !== undefined) {
59 await update($, sessao, s => (feita.fim > s.fim ? { ...NOVA, ...feita } : s))
60 }
61}
62
63// O contexto encolheu: o ctx conhecido zera até o próximo turno.
64async function zeraCtx($: EngineInterface) {
65 const s = await read($, sessao)
66 const zerada: Sessao = { ...s, ctx: 0 }
67 await update($, sessao, () => zerada)
68
69 if (s.fim > 0) {
70 await guarda($, zerada)
71 }
72}
73
74async function poda($: EngineInterface) {
75 const chaves = (await $.store.keys()).filter(chave => chave.startsWith(CHAVE_SESSAO))
76
77 if (chaves.length <= MAX_SESSOES) {
78 return
79 }
80
81 const fins = await Promise.all(
82 chaves.map(async chave => ({ chave, fim: ((await $.store.get(chave)) as Guardada).fim })),
83 )
84 fins.sort((a, b) => b.fim - a.fim)
85 await Promise.all(fins.slice(MAX_SESSOES).map(velha => $.store.delete(velha.chave)))
86}
87
88export const register: Register = on => {
89 on('session.start', async ($, e, next) => {
90 ttlMin = Number(await $.store.get('ttl')) || TTL_PADRAO_MIN
91 avisa = (await $.store.get('aviso')) !== false
92 await $.command.register({
93 name: 'cache-frio',
94 description: 'Aviso de cache frio: estado, ttl em minutos, off e on',
95 argumentHint: '[ttl <minutos> | off | on]',
96 })
97 await restaura($)
98 await poda($)
99
100 return next(e)
101 })
102
103 on('command.run', { command: 'cache-frio' }, async ($, e) => {
104 const [acao, valor] = e.args.trim().toLowerCase().split(/\s+/)
105
106 if (acao === 'ttl') {
107 const min = Number(valor)
108
109 if (!Number.isInteger(min) || min < 1) {
110 return { text: 'Use /cache-frio ttl <minutos>, com um número inteiro a partir de 1.' }
111 }
112
113 ttlMin = min
114 await $.store.set('ttl', min)
115 } else if (acao === 'off' || acao === 'on') {
116 avisa = acao === 'on'
117 await $.store.set('aviso', avisa)
118 } else if (acao) {
119 return { text: 'Use /cache-frio, /cache-frio ttl <minutos>, /cache-frio off ou /cache-frio on.' }
120 }
121
122 const estado = (await linha($)) ?? 'sem turno nesta sessão ainda'
123
124 return { text: `${estado} · ttl ${ttlMin} min · aviso ${avisa ? 'ligado' : 'desligado'}` }
125 })
126
127 on('turn.start', async ($, e, next) => {
128 await update($, sessao, s => ({ ...s, rodando: true }))
129
130 return next(e)
131 })
132
133 // O uso de `turn.complete` soma todas as requisições do turno: serve para o
134 // hit, não para o ctx, que é o `context.tokens` da sessão (última requisição).
135 on('turn.complete', async ($, e, next) => {
136 if (e.agentId === undefined) {
137 const [agora, uso, s] = await Promise.all([$.clock.now(), $.session.usage(), read($, sessao)])
138 const lidos = e.usage?.cache_read_input_tokens ?? 0
139 const entrada = lidos + (e.usage?.cache_creation_input_tokens ?? 0) + (e.usage?.input_tokens ?? 0)
140 const fechada: Sessao = {
141 ...NOVA,
142 fim: agora,
143 ctx: uso.context.tokens ?? s.ctx,
144 hit: entrada > 0 ? Math.round((100 * lidos) / entrada) : (s.hit ?? null),
145 }
146 await update($, sessao, () => fechada)
147 await guarda($, fechada)
148 }
149
150 return next(e)
151 })
152
153 on('session.compact', async ($, e, next) => {
154 const feito = await next(e)
155
156 if (e.agentId === undefined && e.trigger !== 'precompute' && feito.skip === undefined) {
157 await zeraCtx($)
158 }
159
160 return feito
161 })
162
163 // A sessão que segue (depois de /clear, de uma retomada ou de relançar o app)
164 // tem outro histórico: o estado zera e volta do store pelo id dela.
165 on('session.end', async ($, e, next) => {
166 await update($, sessao, () => NOVA)
167
168 return next(e)
169 })
170
171 on('prompt.submit', async ($, e, next) => {
172 // Só o Enter do usuário com a sessão ociosa (`composer` no terminal, `sdk` no
173 // app desktop); comando de barra passa, para /compact e /clear nunca serem segurados.
174 const doUsuario = e.origin.kind === 'composer' || e.origin.kind === 'sdk'
175
176 if (!avisa || !doUsuario || e.turnId !== undefined || e.text.startsWith('/')) {
177 return next(e)
178 }
179
180 let s = await read($, sessao)
181
182 // Retomada sem `session.start` (troca de sessão no mesmo processo).
183 if (s.fim === 0) {
184 await restaura($)
185 s = await read($, sessao)
186 }
187
188 if (s.fim === 0 || s.liberado || s.ctx <= CTX_MINIMO) {
189 return next(e)
190 }
191
192 const agora = await $.clock.now()
193 const parado = agora - s.fim
194
195 if (parado <= ttlMin * MINUTO) {
196 return next(e)
197 }
198
199 const libera = () => update($, sessao, x => ({ ...x, liberado: true }))
200
201 if (s.avisadoEm > 0 && agora - s.avisadoEm <= JANELA_SEGUNDO_ENVIO_MS) {
202 await libera()
203
204 return next(e)
205 }
206
207 const custo = `Cache frio: parado há ${duracao(parado)}. Este envio regrava ~${k(s.ctx)} tokens.`
208 const temAnexo = (e.attachments?.length ?? 0) > 0
209 // Só segura o envio com o texto já de volta no campo: anexo não volta por
210 // `prompt.fill`, e um campo que recusa o texto também não (diálogo aberto, ou
211 // o app desktop, que não tem campo na engine: no_composer). Aí só avisa.
212 const voltou = !temAnexo && (await $.prompt.fill({ text: e.text })).isFilled
213
214 if (!voltou) {
215 await libera()
216 $.ui.toast(custo, { timeoutMs: 8000 })
217
218 return next(e)
219 }
220
221 await update($, sessao, x => ({ ...x, avisadoEm: agora }))
222
223 return { drop: `${custo} Enter de novo envia; ou compacte antes (/compact) ou abra sessão nova.` }
224 })
225}
226types/index.d.ts 22 lines1export type Sessao = {
2 /** Fim do último turno da conversa principal, em ms; 0 antes do primeiro. */
3 fim: number
4 /** Tokens de entrada da última requisição principal; 0 quando desconhecido. */
5 ctx: number
6 /** Parte da entrada do último turno principal lida do cache, de 0 a 100; null sem dado. */
7 hit: number | null
8 rodando: boolean
9 /** Quando o envio frio foi segurado, em ms; 0 se ainda não foi. */
10 avisadoEm: number
11 /** O período frio atual já foi avisado e liberado. */
12 liberado: boolean
13}
14
15declare module 'claude-code' {
16 interface PluginState {
17 'cache-frio': {
18 sessao: Sessao
19 }
20 }
21}
22