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

Mods para o Claude Code.
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.
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│
╰──────────────────────────────────────────────────────────────╯
/progresso-demo simula uma lista pra você ver a faixaA faixa aparece quando o Claude monta uma lista de tarefas (pedidos com várias etapas).
claude plugin marketplace update claude-mods
claude plugin update progresso-tarefas@claude-mods
MIT
hooks/register.tsx 198 lines1import { 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}
198types/index.d.ts 15 lines1export 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