SLOPSHOPPER

u

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

newbandguardcommandstatusprompt
v0.6.0no licenseupdated 2026-10-10dandevddqm/automatizacoes/task-progress
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · u
› 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 › /progresso ⎿ u: Barra de progresso escondida. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

automatizacoes

Mods do Claude Code, em português.

ModO que faz
previsao-do-contextoPrevisã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-perguntasAo 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.

Instalaçã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.

Atualizar

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

Testes

claude plugin validate previsao-do-contexto
claude plugin test previsao-do-contexto
Source 2 files
hooks/register.tsx 296 lines
1import { 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}
296
types/index.d.ts 24 lines
1export 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