Barra acima do prompt com % da tarefa, estimativa de duração e uso das janelas de 5 horas e semanal.

Mods do Claude Code, em português.
| Mod | O que faz |
|---|---|
previsao-do-contexto | Previsão do tempo da janela de contexto: ícone e palavra por faixa de uso (🌞 Limpo, ⛅ Nublado, ☔ Chuva, ⚡ Tempestade, 🔥 Compacta logo), porcentagem e tokens, mini gráfico dos últimos 12 turnos e quanto o último turno somou. |
u (pasta task-progress) | Barra da tarefa: etapas E x/y, progresso T, tempo decorrido e estimado, e uso das janelas de 5 horas e semanal na linha de status. |
recomendar-perguntas | Ao fazer uma recomendação, o Claude abre uma janela com 3 a 5 perguntas (ou mais), com opções, antes de seguir. |
Com u e previsao-do-contexto instalados, a barra da tarefa e a previsão aparecem na mesma linha, as tarefas primeiro. O previsao-do-contexto funciona sozinho; sem o u, mostra só a previsão.
No prompt de uma sessão do Claude Code no terminal (aceite com y adicionar o marketplace e escolha o escopo user):
/plugin install previsao-do-contexto --marketplace dandevddqm/automatizacoes
/plugin install u --marketplace dandevddqm/automatizacoes
/plugin install recomendar-perguntas --marketplace dandevddqm/automatizacoes
Depois de instalado no terminal, no escopo de usuário, o mod também carrega no Claude Code desktop dessa máquina. O comando /plugin install não funciona dentro do desktop.
Os mods ficam em arquivos locais de cada computador: instale em cada máquina onde quiser usá-los.
Ao mudar um mod, suba a versão no plugin.json dele; sem isso o Claude Code continua usando a cópia em cache.
claude plugin marketplace update automatizacoes
claude plugin update previsao-do-contexto@automatizacoes --scope user
claude plugin validate previsao-do-contexto
claude plugin test previsao-do-contextohooks/register.tsx 296 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Limit, Task } from '../types'
5
6const idle: Task = {
7 isRunning: false,
8 startedAt: 0,
9 now: 0,
10 total: 0,
11 done: 0,
12 active: 0,
13 calls: 0,
14 lastSeconds: 0,
15 history: [],
16 callsHistory: [],
17}
18
19const task = atom({ plugin: 'u', key: 'task' } as const, idle)
20const limits = atom({ plugin: 'u', key: 'limits' } as const, [] as Limit[])
21const isHidden = atom({ plugin: 'u', key: 'isHidden' } as const, false)
22
23const label: Record<string, string> = { five_hour: '5', seven_day: 'S' }
24
25const TASK_BAR = 12
26const LIMIT_BAR = 6
27
28const bar = (pct: number, width: number) => {
29 const filled = Math.round((Math.max(0, Math.min(100, pct)) / 100) * width)
30
31 return '█'.repeat(filled) + '░'.repeat(width - filled)
32}
33
34const two = (n: number) => String(n).padStart(2, '0')
35
36/** Sempre 7 caracteres: " 00:42" ou "1:02:09". */
37const clock = (seconds: number) => {
38 const s = Math.max(0, Math.round(seconds))
39 const h = Math.floor(s / 3600)
40 const text =
41 h > 0
42 ? `${h}:${two(Math.floor((s % 3600) / 60))}:${two(s % 60)}`
43 : `${two(Math.floor(s / 60))}:${two(s % 60)}`
44
45 return text.padStart(7, ' ')
46}
47
48/** Sempre 5 caracteres: "3d05h", "2h10m", " 45m". */
49const until = (seconds: number) => {
50 const s = Math.max(0, Math.round(seconds))
51 const d = Math.floor(s / 86400)
52 const h = Math.floor((s % 86400) / 3600)
53 const m = Math.floor((s % 3600) / 60)
54 const text = d > 0 ? `${d}d${two(h)}h` : h > 0 ? `${h}h${two(m)}m` : `${m}m`
55
56 return text.padStart(5, ' ')
57}
58
59const percent = (pct: number) => `${String(Math.round(pct)).padStart(3, ' ')}%`
60
61const average = (xs: number[]) => (xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length)
62
63/** Percentual e duração total estimada da tarefa. */
64const estimate = (t: Task) => {
65 const elapsed = Math.max(0, (t.now - t.startedAt) / 1000)
66
67 if (t.total > 0) {
68 const fraction = (t.done + 0.5 * t.active) / t.total
69 const pct = t.done >= t.total ? 100 : Math.min(99, fraction * 100)
70 const total = fraction > 0.05 ? elapsed / fraction : average(t.history)
71
72 return { elapsed, pct, total }
73 }
74
75 const typical = average(t.history)
76
77 if (typical === 0) return { elapsed, pct: undefined, total: 0 }
78
79 return { elapsed, pct: Math.min(95, (elapsed / typical) * 100), total: Math.max(typical, elapsed) }
80}
81
82/**
83 * Etapas da tarefa. Com lista do Claude: feitas/total. Sem lista: ferramentas
84 * já executadas sobre a média das últimas tarefas (só o número, se ainda não há histórico).
85 */
86const steps = (t: Task) => {
87 if (t.total > 0) return `${t.done}/${t.total}`
88
89 const typical = Math.round(average(t.callsHistory))
90
91 if (t.isRunning && typical > 0) return `${t.calls}/${Math.max(typical, t.calls)}`
92 if (t.calls > 0) return `${t.calls}`
93
94 return undefined
95}
96
97// Quadrados coloridos: verde abaixo de 70%, amarelo até 89%, vermelho a partir de 90%.
98const square = (pct: number) => (pct >= 90 ? '🟥' : pct >= 70 ? '🟨' : '🟩')
99
100const colorBar = (pct: number, width: number) => {
101 const filled = Math.round((Math.max(0, Math.min(100, pct)) / 100) * width)
102
103 return square(pct).repeat(filled) + '⬛'.repeat(width - filled)
104}
105
106/** "5 🟩🟩⬛⬛⬛⬛ 29% ↻1h27m · S 🟩🟩🟩🟩⬛⬛ 60% ↻1d06h" */
107const limitsLine = (rl: Limit[], now: number) =>
108 rl
109 .filter(l => l.kind in label)
110 .map(l => {
111 const left = l.resetsAt === undefined ? '' : ` ↻${until((Date.parse(l.resetsAt) - now) / 1000).trim()}`
112
113 return `${label[l.kind]} ${colorBar(l.percentUsed, LIMIT_BAR)} ${percent(l.percentUsed)}${left}`
114 })
115 .join(' · ')
116
117// Os limites 5 e S ficam na linha de status do rodapé, ao lado do modelo.
118async function showLimits($: EngineInterface, hidden?: boolean) {
119 const isOff = hidden ?? (await read($, isHidden))
120 const line = isOff ? '' : limitsLine(await read($, limits), await $.clock.now())
121
122 $.ui.status(line === '' ? undefined : line)
123}
124
125export const register: Register = on => {
126 let stop: { cancel: () => void } | undefined
127
128 on('session.start', async ($, e, next) => {
129 await $.command.register({
130 name: 'progresso',
131 description: 'Mostra ou esconde a barra de progresso da tarefa',
132 })
133
134 const numbers = (value: unknown) =>
135 Array.isArray(value) ? value.filter((n): n is number => typeof n === 'number') : []
136 const history = numbers(await $.store.get('history'))
137 const callsHistory = numbers(await $.store.get('callsHistory'))
138 await update($, task, t => ({ ...t, history, callsHistory }))
139
140 const usage = await $.session.usage()
141 await update($, limits, () => usage.rateLimits.map(l => ({ ...l })))
142 await showLimits($)
143
144 return next(e)
145 })
146
147 on('command.run', { command: 'progresso' }, async $ => {
148 const hidden = await read($, isHidden)
149 await update($, isHidden, () => !hidden)
150 await showLimits($, !hidden)
151
152 return { text: hidden ? 'Barra de progresso visível.' : 'Barra de progresso escondida.' }
153 })
154
155 on('session.measure', async ($, e, next) => {
156 await update($, limits, () => e.rateLimits.map(l => ({ ...l })))
157 await showLimits($)
158
159 return next(e)
160 })
161
162 on('prompt.submit', async ($, e, next) => {
163 const now = await $.clock.now()
164 await update($, task, t => ({
165 ...idle,
166 history: t.history,
167 callsHistory: t.callsHistory,
168 isRunning: true,
169 startedAt: now,
170 now,
171 }))
172
173 stop?.cancel()
174 stop = $.clock.every(1000, async () => {
175 const tick = await $.clock.now()
176 await update($, task, t => (t.isRunning ? { ...t, now: tick } : t))
177 })
178
179 return next(e)
180 })
181
182 on('tool.call', async ($, e, next) => {
183 if (e.tool !== 'TodoWrite') {
184 await update($, task, t => (t.isRunning ? { ...t, calls: t.calls + 1 } : t))
185 }
186
187 return next(e)
188 })
189
190 on('tool.call', { tool: 'TodoWrite' }, async ($, e, next) => {
191 const todos = e.todos
192 await update($, task, t => ({
193 ...t,
194 total: todos.length,
195 done: todos.filter(x => x.status === 'completed').length,
196 active: todos.filter(x => x.status === 'in_progress').length,
197 }))
198
199 return next(e)
200 })
201
202 on('turn.complete', async ($, e, next) => {
203 if (e.agentId !== undefined) return next(e)
204
205 stop?.cancel()
206 stop = undefined
207
208 const now = await $.clock.now()
209 const current = await read($, task)
210 const seconds = Math.round((now - current.startedAt) / 1000)
211 // Só tarefas sem lista de etapas alimentam as estimativas por histórico.
212 const isPlain = current.total === 0 && e.reason === 'answer' && seconds > 0
213 const history = isPlain ? [...current.history, seconds].slice(-10) : current.history
214 const callsHistory =
215 isPlain && current.calls > 0
216 ? [...current.callsHistory, current.calls].slice(-10)
217 : current.callsHistory
218
219 await $.store.set('history', history)
220 await $.store.set('callsHistory', callsHistory)
221 await update($, task, t => ({
222 ...t,
223 isRunning: false,
224 now,
225 done: t.total > 0 && e.reason === 'answer' ? t.total : t.done,
226 active: 0,
227 lastSeconds: seconds,
228 history,
229 callsHistory,
230 }))
231
232 const usage = await $.session.usage()
233 await update($, limits, () => usage.rateLimits.map(l => ({ ...l })))
234 await showLimits($)
235
236 return next(e)
237 })
238
239 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
240 if (e.props.hasSurvey || (await read($, isHidden))) return next(e)
241
242 const t = await read($, task)
243 const { Box, Text, Button } = $.ui.resolve(e)
244 const stepsText = steps(t)
245 let row
246
247 if (t.isRunning) {
248 const s = estimate(t)
249 const left = s.total > 0 ? `−${clock(s.total - s.elapsed).trim()}` : ''
250
251 row = (
252 <Text key="task" wrap="truncate-end">
253 {stepsText === undefined ? null : (
254 <Text>
255 <Text bold>E </Text>
256 <Text color="claude">{stepsText}</Text>
257 {' '}
258 </Text>
259 )}
260 <Text bold>T </Text>
261 <Text color="claude">{bar(s.pct ?? 0, TASK_BAR)}</Text>
262 {s.pct === undefined ? null : <Text color="claude"> {percent(s.pct)}</Text>}
263 <Text dimColor>
264 {' '}{clock(s.elapsed)} {left}
265 </Text>
266 </Text>
267 )
268 } else if (t.lastSeconds > 0) {
269 row = (
270 <Text key="task" dimColor wrap="truncate-end">
271 U {clock(t.lastSeconds)}
272 {stepsText === undefined ? '' : ` E ${stepsText}`}
273 </Text>
274 )
275 } else {
276 return next(e)
277 }
278
279 return (
280 <Box flexDirection="row" flexWrap="nowrap" alignItems="center" justifyContent="space-between" gap={1} overflow="hidden">
281 <Box flexShrink={1} overflow="hidden">
282 {row}
283 </Box>
284 <Button
285 key="hide"
286 label="×"
287 onPress={async () => {
288 await update($, isHidden, () => true)
289 await showLimits($, true)
290 }}
291 />
292 </Box>
293 )
294 })
295}
296types/index.d.ts 24 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Task = {
4 isRunning: boolean
5 startedAt: number
6 now: number
7 total: number
8 done: number
9 active: number
10 /** Ferramentas executadas na tarefa (sem a lista de etapas). */
11 calls: number
12 lastSeconds: number
13 /** Duração (s) das últimas tarefas sem lista de todos, base da estimativa. */
14 history: number[]
15 /** Ferramentas executadas nas últimas tarefas sem lista de etapas. */
16 callsHistory: number[]
17}
18
19declare module 'claude-code' {
20 interface PluginState {
21 'u': { task: Task; limits: Limit[]; isHidden: boolean }
22 }
23}
24