SLOPSHOPPER

progresso-tarefas

Mostra só as tarefas principais com barra de progresso e % acima do prompt

newbandguardcommandtoaststatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · progresso-tarefas
› 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-demo ⎿ progresso-tarefas: Demo iniciada: veja a faixa acima do prompt (10s). ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-mods

Mods para o Claude Code.

Instalar

No Claude Code (terminal ou app):

/plugin marketplace add rodrigofaerman/claude-mods
/plugin install progresso-tarefas@claude-mods

Ou pelo terminal:

claude plugin marketplace add rodrigofaerman/claude-mods
claude plugin install progresso-tarefas@claude-mods

Depois rode /reload-plugins ou abra uma sessão nova.

Mods

progresso-tarefas

Em vez de acompanhar cada comando, veja só as tarefas principais com progresso:

╭──────────────────────────────────────────────────────────────╮
│  ✓ Tudo pronto   Build a weather dashboard…     levou 1m 47s │
│ 4 de 4 tarefas  ████████████████████████████████████████ 100%│
│ ✓ Pick the page style and layout   ████████████████████ Feito│
│ ✓ Check how the page gets weather  ████████████████████ Feito│
│ ◐ Build the weather dashboard      ██████████░░░░░ Em andamento│
│ ○ Publish it and share the link    ░░░░░░░░░░░░░░░░░░ Pendente│
╰──────────────────────────────────────────────────────────────╯
  • Faixa acima do prompt com cada tarefa, barra geral com % e tempo
  • % na barra de status
  • Aviso quando tudo termina
  • /progresso-demo simula uma lista pra você ver a faixa

A faixa aparece quando o Claude monta uma lista de tarefas (pedidos com várias etapas).

Atualizar

claude plugin marketplace update claude-mods
claude plugin update progresso-tarefas@claude-mods

Licença

MIT

Source 2 files
hooks/register.tsx 198 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Run, Task, TaskStatus } from '../types'
5
6const run = atom({ plugin: 'progresso-tarefas', key: 'run' } as const, null)
7const isHidden = atom({ plugin: 'progresso-tarefas', key: 'isHidden' } as const, false)
8
9const BAR = 20
10const GREEN = '#3fb950'
11
12const bar = (fraction: number, width: number) => {
13  const full = Math.round(fraction * width)
14  return '█'.repeat(full) + '░'.repeat(width - full)
15}
16
17const duration = (ms: number) => {
18  const s = Math.max(0, Math.round(ms / 1000))
19  return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`
20}
21
22const percent = (tasks: readonly Task[]) =>
23  tasks.length === 0
24    ? 0
25    : Math.round((tasks.filter(t => t.status === 'completed').length / tasks.length) * 100)
26
27const isDone = (r: Run | null) =>
28  r !== null && r.tasks.length > 0 && r.tasks.every(t => t.status === 'completed')
29
30// Escreve a lista nova, atualiza status line e avisa quando tudo terminou.
31async function commit($: EngineInterface, goal: string, tasks: Task[]) {
32  const now = await $.clock.now()
33  const before = await read($, run)
34  // Uma lista nova depois de uma já concluída começa uma nova rodada.
35  const fresh = before === null || isDone(before)
36  const next: Run = {
37    goal: fresh ? goal : before.goal,
38    tasks,
39    startedAt: fresh ? now : before.startedAt,
40    finishedMs: null,
41  }
42  if (isDone(next)) next.finishedMs = now - next.startedAt
43
44  await update($, run, () => next)
45  await update($, isHidden, () => false)
46
47  const done = tasks.filter(t => t.status === 'completed').length
48  $.ui.status(tasks.length ? `${bar(percent(tasks) / 100, 10)} ${percent(tasks)}% · ${done}/${tasks.length}` : undefined)
49
50  if (next.finishedMs !== null && !isDone(before)) {
51    $.ui.toast(`✓ Tudo pronto: ${tasks.length} tarefas em ${duration(next.finishedMs)}`)
52  }
53}
54
55const DEMO = [
56  'Escolher estilo e layout da página',
57  'Ver como a página busca o clima',
58  'Construir o dashboard de clima',
59  'Publicar e compartilhar o link',
60]
61
62// Etapa i da demo: tarefas antes de i feitas, a i em andamento.
63async function demoStep($: EngineInterface, i: number) {
64  const tasks: Task[] = DEMO.map((title, k) => ({
65    id: `demo-${k}`,
66    title,
67    status: k < i ? 'completed' : k === i ? 'in_progress' : 'pending',
68  }))
69  await commit($, 'Demo: dashboard de clima para Nova York', tasks)
70}
71
72export const register: Register = on => {
73  let goal = ''
74
75  on('session.start', async ($, e, next) => {
76    await $.command.register({
77      name: 'progresso-demo',
78      description: 'Simula 4 tarefas avançando pra mostrar a faixa de progresso',
79    })
80    return next(e)
81  })
82
83  on('command.run', { command: 'progresso-demo' }, async $ => {
84    await update($, run, () => null)
85    for (let i = 0; i <= DEMO.length; i++) {
86      $.clock.after(i * 2500, () => void demoStep($, i).catch(() => {}))
87    }
88    return { text: 'Demo iniciada: veja a faixa acima do prompt (10s).' }
89  })
90
91  on('prompt.submit', ($, e, next) => {
92    goal = (e.text.split('\n')[0] ?? '').slice(0, 80)
93    return next(e)
94  })
95
96  // Lista de tarefas no formato TodoWrite: vem inteira a cada chamada.
97  on('tool.call', { tool: 'TodoWrite' }, async ($, e, next) => {
98    const ran = await next(e)
99    if (ran.deny === undefined && !ran.isError) {
100      await commit(
101        $,
102        goal,
103        e.todos.map((t, i) => ({ id: String(i), title: t.content, status: t.status })),
104      ).catch(() => {})
105    }
106    return ran
107  })
108
109  // Lista no formato TaskCreate/TaskUpdate: uma tarefa por chamada.
110  on('tool.call', { tool: 'TaskCreate' }, async ($, e, next) => {
111    const ran = await next(e)
112    if (ran.deny === undefined && !ran.isError) {
113      const id = (ran.result as { task?: { id?: string } }).task?.id ?? String(Date.now())
114      const current = await read($, run)
115      const base = current === null || isDone(current) ? [] : current.tasks
116      await commit($, goal, [...base, { id, title: e.subject, status: 'pending' }]).catch(() => {})
117    }
118    return ran
119  })
120
121  on('tool.call', { tool: 'TaskUpdate' }, async ($, e, next) => {
122    const ran = await next(e)
123    const current = await read($, run)
124    if (ran.deny === undefined && !ran.isError && current !== null) {
125      const tasks = current.tasks
126        .filter(t => !(t.id === e.taskId && e.status === 'deleted'))
127        .map(t =>
128          t.id === e.taskId
129            ? {
130                ...t,
131                title: e.subject ?? t.title,
132                status: (e.status && e.status !== 'deleted' ? e.status : t.status) as TaskStatus,
133              }
134            : t,
135        )
136      await commit($, goal, tasks).catch(() => {})
137    }
138    return ran
139  })
140
141  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
142    const r = await read($, run)
143    if (e.props.hasSurvey || r === null || r.tasks.length === 0 || (await read($, isHidden))) {
144      return next(e)
145    }
146
147    const { Box, Button, Text } = $.ui.resolve(e)
148    const pct = percent(r.tasks)
149    const done = r.tasks.filter(t => t.status === 'completed').length
150    const finished = r.finishedMs !== null
151    const elapsed = finished ? (r.finishedMs as number) : (await $.clock.now()) - r.startedAt
152
153    const label: Record<TaskStatus, string> = {
154      completed: 'Feito',
155      in_progress: 'Em andamento',
156      pending: 'Pendente',
157    }
158    const icon: Record<TaskStatus, string> = { completed: '✓', in_progress: '◐', pending: '○' }
159    const fill: Record<TaskStatus, number> = { completed: 1, in_progress: 0.5, pending: 0 }
160
161    return (
162      <Box flexDirection="column" borderStyle="round" borderColor={finished ? GREEN : 'gray'} paddingX={1}>
163        <Box justifyContent="space-between">
164          <Box gap={1}>
165            <Text backgroundColor={finished ? GREEN : undefined} color={finished ? 'black' : 'yellow'} bold>
166              {finished ? ' ✓ Tudo pronto ' : ' ◐ Trabalhando '}
167            </Text>
168            <Text bold wrap="truncate">{r.goal}</Text>
169          </Box>
170          <Text dimColor>{finished ? `levou ${duration(elapsed)}` : duration(elapsed)}</Text>
171        </Box>
172        <Box gap={1}>
173          <Text dimColor>{`${done} de ${r.tasks.length} tarefas`}</Text>
174          <Text color={GREEN}>{bar(pct / 100, BAR * 2)}</Text>
175          <Text bold>{`${pct}%`}</Text>
176        </Box>
177        {r.tasks.map(t => (
178          <Box key={t.id} gap={1}>
179            <Text color={t.status === 'completed' ? GREEN : t.status === 'in_progress' ? 'yellow' : 'gray'}>
180              {icon[t.status]}
181            </Text>
182            <Box width={40}>
183              <Text wrap="truncate" dimColor={t.status === 'pending'}>{t.title}</Text>
184            </Box>
185            <Text color={t.status === 'completed' ? GREEN : 'yellow'}>{bar(fill[t.status], BAR)}</Text>
186            <Text dimColor={t.status !== 'completed'}>{label[t.status]}</Text>
187          </Box>
188        ))}
189        {finished ? (
190          <Box>
191            <Button key="hide" label="Ocultar" onPress={() => update($, isHidden, () => true)} />
192          </Box>
193        ) : null}
194      </Box>
195    )
196  })
197}
198
types/index.d.ts 15 lines
1export type TaskStatus = 'pending' | 'in_progress' | 'completed'
2export type Task = { id: string; title: string; status: TaskStatus }
3export type Run = {
4  goal: string
5  tasks: Task[]
6  startedAt: number
7  finishedMs: number | null
8}
9
10declare module 'claude-code' {
11  interface PluginState {
12    'progresso-tarefas': { run: Run | null; isHidden: boolean }
13  }
14}
15