SLOPSHOPPER

agentes-pixel

Painel lateral com subagents em pixel art, bolinhas de status das tarefas e barra do plano acima do prompt

newpanebandrowsguardcommand
v0.1.0no licenseupdated 2026-10-26DiogoHSM/claude-mods/agentes-pixel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agentes-pixel
│ ┃ Agentes ✕ › fix the failing auth test and add an audit log call │ ┃ Subagents │ ┃ ⏺ Read(src/auth.ts) │ ┃ Nenhum subagent nesta sessão ainda. ⎿ 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 │ │ › /agentes │ ⎿ agentes-pixel: Painel de agentes aberto. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Agentes
Subagents Nenhum subagent nesta sessão ainda.
README

claude-mods

Mods pessoais para Claude Code (terminal e aba Code do app desktop).

Mods

ModO que faz
agentes-pixelPainel lateral com pixel art de cada subagent (modelo, status, tempo, tokens, resumo), bolinhas de status das tarefas e barra do plano acima do prompt. /agentes reabre o painel.
polimento-visualPill de contexto usado/total abaixo do prompt, borda laranja nas suas mensagens e ícones coloridos por tipo de ferramenta.

Instalação

claude plugin marketplace add DiogoHSM/claude-mods
claude plugin install agentes-pixel@diogo-mods --scope user
claude plugin install polimento-visual@diogo-mods --scope user

Ou, no prompt do Claude Code:

/plugin install agentes-pixel --marketplace DiogoHSM/claude-mods
/plugin install polimento-visual --marketplace DiogoHSM/claude-mods

No app desktop: + ao lado do prompt → Plugins → Add marketplace com DiogoHSM/claude-mods → Add plugin.

Desenvolvimento local

claude plugin marketplace add ~/Developer/Github/claude-mods
claude plugin install agentes-pixel@diogo-mods --scope user

Depois de editar um mod, rode /reload-plugins.

Validação

claude plugin validate agentes-pixel
claude plugin test agentes-pixel
Source 2 files
hooks/register.tsx 579 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { AgentRow, AgentState, TaskDot, TaskState } from '../types'
5
6const PANE = 'agentes-pixel'
7const MAIN = 'main'
8
9const agents = atom({ plugin: 'agentes-pixel', key: 'agents' } as const, [])
10const tasks = atom({ plugin: 'agentes-pixel', key: 'tasks' } as const, {})
11const now = atom({ plugin: 'agentes-pixel', key: 'now' } as const, 0)
12
13const HEX: Record<TaskState, string> = {
14  pending: '#888780',
15  running: '#D97757',
16  done: '#639922',
17  error: '#E24B4A',
18}
19const THEME: Record<TaskState, 'inactive' | 'claude' | 'success' | 'error'> = {
20  pending: 'inactive',
21  running: 'claude',
22  done: 'success',
23  error: 'error',
24}
25const TASK_LABEL: Record<TaskState, string> = {
26  pending: 'planejado',
27  running: 'executando',
28  done: 'concluído',
29  error: 'erro',
30}
31const AGENT_LABEL: Record<AgentState, string> = {
32  running: 'executando',
33  done: 'concluído',
34  error: 'erro',
35}
36const AGENT_TASK: Record<AgentState, TaskState> = {
37  running: 'running',
38  done: 'done',
39  error: 'error',
40}
41
42type Creature = 'owl' | 'fox' | 'bird' | 'cat' | 'bot'
43
44const SPRITES: Record<Creature, { colors: Record<string, string>; grid: string[] }> = {
45  owl: {
46    colors: { a: '#534AB7', b: '#AFA9EC', e: '#FAEEDA', k: '#26215C', y: '#EF9F27' },
47    grid: ['.a....a.', 'aaaaaaaa', 'aeekkeea', 'aeekkeea', 'aaayyaaa', 'abbbbbba', 'abbbbbba', '.a.aa.a.'],
48  },
49  fox: {
50    colors: { a: '#D85A30', b: '#FAECE7', k: '#4A1B0C' },
51    grid: ['a......a', 'aa....aa', 'aaaaaaaa', 'akaaaaka', 'abaaaaba', '.bbkkbb.', '..bbbb..', '...aa...'],
52  },
53  bird: {
54    colors: { a: '#1D9E75', b: '#9FE1CB', k: '#04342C', y: '#EF9F27' },
55    grid: ['........', '..aaaa..', '.aaaaaa.', '.akaaaay', '.aaaaaa.', '.abbbba.', '..bbbb..', '..y..y..'],
56  },
57  cat: {
58    colors: { a: '#D4537E', b: '#FBEAF0', k: '#4B1528' },
59    grid: ['a......a', 'aa....aa', 'aaaaaaaa', 'akaaaaka', 'aaabbaaa', 'abbkkbba', '.aaaaaa.', '.a....a.'],
60  },
61  bot: {
62    colors: { a: '#888780', b: '#D3D1C7', k: '#2C2C2A' },
63    grid: ['...k....', '.aaaaaa.', '.akaaka.', '.aaaaaa.', '..abba..', 'aaaaaaaa', 'a.aaaa.a', '..a..a..'],
64  },
65}
66
67const GLYPH: Record<Creature, string> = { owl: '🦉', fox: '🦊', bird: '🐦', cat: '🐱', bot: '🤖' }
68
69function family(model: string): { name: string; creature: Creature } {
70  const id = model.toLowerCase()
71  if (id.includes('opus')) return { name: 'Opus', creature: 'owl' }
72  if (id.includes('sonnet')) return { name: 'Sonnet', creature: 'fox' }
73  if (id.includes('haiku')) return { name: 'Haiku', creature: 'bird' }
74  if (id.includes('fable')) return { name: 'Fable', creature: 'cat' }
75
76  return { name: model || 'modelo', creature: 'bot' }
77}
78
79function sprite(creature: Creature, state: AgentState): string {
80  const { colors, grid } = SPRITES[creature]
81  let cells = ''
82  grid.forEach((row, y) =>
83    [...row].forEach((ch, x) => {
84      const fill = ch === 'k' && state === 'error' ? '#E24B4A' : colors[ch]
85      if (fill) cells += `<rect x="${x + 1}" y="${y + 1}" width="1" height="1" fill="${fill}"/>`
86    }),
87  )
88  const bob =
89    state === 'running'
90      ? '<animateTransform attributeName="transform" type="translate" values="0 0;0 -0.6" dur="0.8s" calcMode="discrete" repeatCount="indefinite"/>'
91      : ''
92  const badge =
93    state === 'done'
94      ? `<rect x="8" y="8" width="2" height="2" fill="${HEX.done}"/>`
95      : state === 'error'
96        ? `<rect x="8" y="8" width="2" height="2" fill="${HEX.error}"/>`
97        : ''
98
99  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" width="36" height="36" shape-rendering="crispEdges"><g>${cells}${bob}</g>${badge}</svg>`
100}
101
102function escape(text: string): string {
103  return text.replace(/[<>&"']/g, ch => `&#${ch.charCodeAt(0)};`)
104}
105
106function elapsed(ms: number): string {
107  const s = Math.max(0, Math.round(ms / 1000))
108  if (s < 60) return `${s}s`
109  const m = Math.floor(s / 60)
110  if (m < 60) return `${m}m ${s % 60}s`
111
112  return `${Math.floor(m / 60)}h ${m % 60}m`
113}
114
115function tokens(n: number): string {
116  if (n < 1000) return `${n}`
117  if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`
118
119  return `${(n / 1_000_000).toFixed(1)}M`
120}
121
122function oneLine(text: string, max = 90): string {
123  const line = text.replace(/\s+/g, ' ').trim()
124
125  return line.length > max ? `${line.slice(0, max - 1)}…` : line
126}
127
128function toTaskState(status: unknown): TaskState {
129  if (status === 'in_progress') return 'running'
130  if (status === 'completed') return 'done'
131
132  return 'pending'
133}
134
135function moveTo(task: TaskDot, state: TaskState, at: number): TaskDot {
136  if (task.state === state) return task
137  const next: TaskDot = { ...task, state }
138  if (state === 'running') next.startedAt = at
139  if (state === 'done' || state === 'error') next.endedAt = at
140
141  return next
142}
143
144function replaceAll(
145  previous: TaskDot[],
146  items: { subject: string; status: unknown; active?: string }[],
147  at: number,
148): TaskDot[] {
149  return items.map((item, i) => {
150    const old = previous.find(one => one.subject === item.subject)
151    const base: TaskDot = old ?? { id: `p${i}`, subject: item.subject, state: 'pending' }
152
153    return moveTo({ ...base, id: `p${i}`, active: item.active ?? base.active }, toTaskState(item.status), at)
154  })
155}
156
157function applyPlan(previous: TaskDot[], e: Record<string, unknown>, at: number): TaskDot[] {
158  let list = previous
159  if (Array.isArray(e.items)) {
160    const items = e.items.map((item: { content?: unknown; status?: unknown }) => ({
161      subject: String(item.content ?? ''),
162      status: item.status,
163    }))
164    list = replaceAll(list, items, at)
165  }
166  if (e.advance === true) {
167    const current = list.findIndex(task => task.state === 'running')
168    const after = list.findIndex((task, i) => i > current && task.state === 'pending')
169    list = list.map((task, i) =>
170      i === current ? moveTo(task, 'done', at) : i === after ? moveTo(task, 'running', at) : task,
171    )
172  }
173  if (Array.isArray(e.done)) {
174    const done = new Set(e.done.map(n => Number(n) - 1))
175    list = list.map((task, i) => (done.has(i) ? moveTo(task, 'done', at) : task))
176  }
177  if (typeof e.start === 'number') {
178    const start = e.start - 1
179    list = list.map((task, i) => (i === start ? moveTo(task, 'running', at) : task))
180  }
181
182  return list
183}
184
185function failRunning(list: TaskDot[] | undefined, at: number): TaskDot[] | undefined {
186  return list?.map(task => (task.state === 'running' ? moveTo(task, 'error', at) : task))
187}
188
189function dotsSvg(list: TaskDot[]): string {
190  const dots = list
191    .map((task, i) => {
192      const cx = 7 + i * 16
193      const title = `<title>${i + 1}. ${escape(task.subject)} — ${TASK_LABEL[task.state]}</title>`
194      const shape =
195        task.state === 'pending'
196          ? `<circle cx="${cx}" cy="8" r="5" fill="none" stroke="${HEX.pending}" stroke-width="1.5"/>`
197          : `<circle cx="${cx}" cy="8" r="5.5" fill="${HEX[task.state]}">${
198              task.state === 'running'
199                ? '<animate attributeName="opacity" values="1;0.35;1" dur="1.2s" repeatCount="indefinite"/>'
200                : ''
201            }</circle>`
202
203      return `<g>${title}${shape}</g>`
204    })
205    .join('')
206
207  return `<svg xmlns="http://www.w3.org/2000/svg" width="${list.length * 16}" height="16">${dots}</svg>`
208}
209
210const PLAN_W = 700
211
212function planSvg(list: TaskDot[]): string {
213  const n = list.length
214  const gap = 6
215  const width = (PLAN_W - gap * (n - 1)) / n
216  const bars = list
217    .map((task, i) => {
218      const fill = task.state === 'pending' ? 'var(--empty)' : HEX[task.state]
219      const pulse =
220        task.state === 'running'
221          ? '<animate attributeName="opacity" values="1;0.45;1" dur="1.4s" repeatCount="indefinite"/>'
222          : ''
223
224      return `<rect x="${i * (width + gap)}" y="2" width="${width}" height="8" rx="4" fill="${fill}">${pulse}</rect>`
225    })
226    .join('')
227
228  return `<svg xmlns="http://www.w3.org/2000/svg" width="10000" height="12" viewBox="0 0 ${PLAN_W} 12" preserveAspectRatio="none"><style>svg{--empty:#D3D1C7}@media (prefers-color-scheme:dark){svg{--empty:#444441}}</style>${bars}</svg>`
229}
230
231function stepLine(task: TaskDot, i: number, n: number, at: number): string {
232  const time =
233    task.state === 'running' && task.startedAt
234      ? ` · há ${elapsed(at - task.startedAt)}`
235      : task.state === 'done' && task.startedAt && task.endedAt
236        ? ` · em ${elapsed(task.endedAt - task.startedAt)}`
237        : ''
238
239  return `${i + 1}/${n} · ${task.subject} — ${TASK_LABEL[task.state]}${time}`
240}
241
242async function showAgent($: EngineInterface, id: string) {
243  const row = (await read($, agents)).find(one => one.id === id)
244  if (row?.isExpanded) {
245    await update($, agents, list => list.map(one => (one.id === id ? { ...one, isExpanded: false } : one)))
246    return
247  }
248  const shown = await $.ui.view({ agentId: id })
249  if (shown.deny === undefined) return
250  if (!row?.result) {
251    $.ui.toast(
252      row?.state === 'running'
253        ? 'O agent ainda está rodando; o resultado aparece aqui quando terminar.'
254        : row
255          ? 'A resposta do agent está chegando; tente de novo em instantes.'
256          : shown.deny,
257    )
258    return
259  }
260  await update($, agents, list => list.map(one => (one.id === id ? { ...one, isExpanded: true } : one)))
261}
262
263let isOpen = false
264let ticker: Timer | undefined
265
266async function openPane($: EngineInterface) {
267  if (isOpen) return
268  isOpen = true
269  await $.ui.open({ id: PANE, title: 'Agentes' })
270}
271
272async function finish($: EngineInterface, ended: Map<string, AgentState>, at: number) {
273  await update($, agents, list =>
274    list.map(row => {
275      const state = ended.get(row.id)
276
277      return state && row.state === 'running' ? { ...row, state, endedAt: at } : row
278    }),
279  )
280  await update($, tasks, plan => {
281    const next = { ...plan }
282    for (const [id, state] of ended) {
283      const failed = state === 'error' ? failRunning(next[id], at) : next[id]
284      if (failed) next[id] = failed
285    }
286
287    return next
288  })
289}
290
291async function tick($: EngineInterface) {
292  const rows = await read($, agents)
293  const plan = await read($, tasks)
294  const isBusy =
295    rows.some(row => row.state === 'running') ||
296    Object.values(plan).some(list => list.some(task => task.state === 'running'))
297  if (!isBusy) return
298  const at = await $.clock.now()
299  await update($, now, () => at)
300  if (!rows.some(row => row.state === 'running')) return
301  const live = await $.agent.list()
302  const ended = new Map<string, AgentState>()
303  for (const info of live) {
304    if (info.status === 'completed') ended.set(info.id, 'done')
305    if (info.status === 'failed' || info.status === 'killed') ended.set(info.id, 'error')
306  }
307  if (ended.size > 0) await finish($, ended, at)
308}
309
310async function setTasks($: EngineInterface, owner: string, fn: (list: TaskDot[], at: number) => TaskDot[]) {
311  const at = await $.clock.now()
312  await update($, tasks, plan => ({ ...plan, [owner]: fn(plan[owner] ?? [], at) }))
313}
314
315export const register: Register = on => {
316  on('session.start', async ($, e, next) => {
317    await $.command.register({ name: 'agentes', description: 'Abre o painel de agentes e tarefas' })
318    ticker?.cancel()
319    ticker = $.clock.every(1000, () => void tick($))
320    if ((await read($, agents)).length > 0) void openPane($)
321
322    return next(e)
323  })
324
325  on('command.run', { command: 'agentes' }, async $ => {
326    isOpen = false
327    await openPane($)
328
329    return { text: 'Painel de agentes aberto.' }
330  })
331
332  on('agent.spawn', async ($, e, next) => {
333    const spawned = await next(e)
334    if (spawned.deny !== undefined || spawned.agentId === undefined) return spawned
335    const row: AgentRow = {
336      id: spawned.agentId,
337      toolUseId: e.tool_use_id,
338      description: e.description,
339      summary: oneLine(e.prompt),
340      type: e.subagentType,
341      model: spawned.model,
342      state: 'running',
343      startedAt: await $.clock.now(),
344      tokens: 0,
345    }
346    await update($, agents, list => [...list.filter(one => one.id !== row.id), row].slice(-30))
347    void openPane($)
348
349    return spawned
350  }).catch(($, e, next) => next(e))
351
352  on('turn.step', async function* ($, e, next) {
353    const step = yield* next(e)
354    const id = e.agentId
355    const usage = step.usage
356    if (id !== undefined && usage) {
357      const spent =
358        usage.input_tokens + usage.output_tokens + usage.cache_creation_input_tokens + usage.cache_read_input_tokens
359      await update($, agents, list =>
360        list.map(row => (row.id === id ? { ...row, tokens: row.tokens + spent, model: usage.model || row.model } : row)),
361      )
362    }
363
364    return step
365  })
366
367  on('turn.complete', async ($, e, next) => {
368    const done = await next(e)
369    const at = await $.clock.now()
370    if (e.agentId !== undefined) {
371      const state: AgentState = e.reason === 'answer' ? 'done' : 'error'
372      await finish($, new Map([[e.agentId, state]]), at)
373      const id = e.agentId
374      if (done.text) await update($, agents, list => list.map(row => (row.id === id ? { ...row, result: done.text } : row)))
375    } else if (e.reason === 'error') {
376      await update($, tasks, plan => ({ ...plan, [MAIN]: failRunning(plan[MAIN], at) ?? [] }))
377    }
378
379    return done
380  })
381
382  on('tool.call', { tool: 'TaskCreate' }, async ($, e, next) => {
383    const ran = await next(e)
384    const created = ran.deny === undefined ? (ran.result as { task?: { id?: string } } | undefined) : undefined
385    const id = created?.task?.id
386    if (id !== undefined) {
387      const task: TaskDot = { id, subject: e.subject, active: e.activeForm, state: 'pending' }
388      await setTasks($, e.agentId ?? MAIN, list => [...(list.every(one => one.state === 'done') ? [] : list), task])
389    }
390
391    return ran
392  }).catch(($, e, next) => next(e))
393
394  on('tool.call', { tool: 'TaskUpdate' }, async ($, e, next) => {
395    const ran = await next(e)
396    if (ran.deny === undefined && !ran.isError) {
397      await setTasks($, e.agentId ?? MAIN, (list, at) =>
398        e.status === 'deleted'
399          ? list.filter(task => task.id !== e.taskId)
400          : list.map(task =>
401              task.id === e.taskId
402                ? moveTo({ ...task, subject: e.subject ?? task.subject, active: e.activeForm ?? task.active }, e.status ? toTaskState(e.status) : task.state, at)
403                : task,
404            ),
405      )
406    }
407
408    return ran
409  }).catch(($, e, next) => next(e))
410
411  on('tool.call', { tool: 'TodoWrite' }, async ($, e, next) => {
412    const ran = await next(e)
413    if (ran.deny === undefined && !ran.isError) {
414      await setTasks($, e.agentId ?? MAIN, (list, at) =>
415        replaceAll(list, e.todos.map(todo => ({ subject: todo.content, status: todo.status, active: todo.activeForm })), at),
416      )
417    }
418
419    return ran
420  }).catch(($, e, next) => next(e))
421
422  on('tool.call', { tool: 'mcp__ccd_turn__update_plan' }, async ($, e, next) => {
423    const ran = await next(e)
424    if (ran.deny === undefined && !ran.isError) {
425      await setTasks($, e.agentId ?? MAIN, (list, at) => applyPlan(list, e, at))
426    }
427
428    return ran
429  }).catch(($, e, next) => next(e))
430
431  on('tool.call', { tool: 'mcp__ccd_turn__wrap_up' }, async ($, e, next) => {
432    const ran = await next(e)
433    if (ran.deny === undefined && !ran.isError) {
434      await setTasks($, e.agentId ?? MAIN, (list, at) =>
435        list.map(task => (task.state === 'done' || task.state === 'error' ? task : moveTo(task, 'done', at))),
436      )
437    }
438
439    return ran
440  }).catch(($, e, next) => next(e))
441
442  on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
443    if (e.props.tool !== 'mcp__ccd_turn__update_plan' || e.props.isExpanded) return next(e)
444    const { Box } = $.ui.resolve(e)
445
446    return <Box />
447  })
448
449  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
450    const { Box, Text, Button } = $.ui.resolve(e)
451    const Svg = e.surface === 'terminal' ? undefined : $.ui.resolve(e).Svg
452    const rows = await read($, agents)
453    const plan = await read($, tasks)
454    const at = Math.max(await read($, now), ...rows.map(row => row.endedAt ?? row.startedAt))
455    const ordered = [...rows].sort(
456      (a, b) => Number(b.state === 'running') - Number(a.state === 'running') || b.startedAt - a.startedAt,
457    )
458    const owners = Object.entries(plan).filter(([, list]) => list.length > 0)
459    const ownerLabel = (owner: string) => {
460      if (owner === MAIN) return 'Principal'
461      const row = rows.find(one => one.id === owner)
462
463      return row ? `${family(row.model).name} · ${row.description}` : 'Subagent'
464    }
465
466    return (
467      <Box flexDirection="column" gap={1}>
468        <Text dimColor size="caption">
469          Subagents
470        </Text>
471        {ordered.length === 0 && <Text dimColor>Nenhum subagent nesta sessão ainda.</Text>}
472        {ordered.slice(0, 12).map(row => {
473          const { name, creature } = family(row.model)
474          const color = THEME[AGENT_TASK[row.state]]
475
476          return (
477            <Box key={row.id} flexDirection="row" gap={1} borderStyle="round" borderColor={color} paddingX={1}>
478              {Svg ? (
479                <Svg source={sprite(creature, row.state)} alt={`${name}, ${AGENT_LABEL[row.state]}`} width={36} height={36} isInteractive />
480              ) : (
481                <Text>{GLYPH[creature]}</Text>
482              )}
483              <Box flexDirection="column" flexGrow={1}>
484                <Text>
485                  <Text bold>{name}</Text> <Text color={color}>● {AGENT_LABEL[row.state]}</Text>{' '}
486                  <Text dimColor>{row.type}</Text>
487                </Text>
488                <Text>{row.description}</Text>
489                <Text dimColor>{row.summary}</Text>
490                <Text dimColor tabular>
491                  {elapsed((row.endedAt ?? at) - row.startedAt)} · ~{tokens(row.tokens)} tokens
492                </Text>
493                {row.isExpanded && row.result && (
494                  <Box borderStyle="round" borderColor="subtle" paddingX={1} marginTop={1}>
495                    <Text>{row.result.length > 4000 ? `${row.result.slice(0, 4000)}…` : row.result}</Text>
496                  </Box>
497                )}
498              </Box>
499              <Button key={`ver-${row.id}`} label={row.isExpanded ? 'Ocultar' : 'Ver'} plain onPress={() => void showAgent($, row.id)} />
500            </Box>
501          )
502        })}
503        {owners.length > 0 && (
504          <Text dimColor size="caption">
505            Tarefas
506          </Text>
507        )}
508        {owners.map(([owner, list]) => (
509          <Box key={`t-${owner}`} flexDirection="row" gap={1} alignItems="center">
510            <Box width={22}>
511              <Text dimColor>{oneLine(ownerLabel(owner), 22)}</Text>
512            </Box>
513            {Svg ? (
514              <Svg
515                source={dotsSvg(list)}
516                alt={list.map(task => `${task.subject}: ${TASK_LABEL[task.state]}`).join('; ')}
517                height={16}
518                isInteractive
519              />
520            ) : (
521              <Box flexDirection="row">
522                {list.map(task => (
523                  <Text color={THEME[task.state]}>{task.state === 'pending' ? '○ ' : '● '}</Text>
524                ))}
525              </Box>
526            )}
527          </Box>
528        ))}
529      </Box>
530    )
531  })
532
533  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
534    const list = (await read($, tasks))[MAIN] ?? []
535    if (e.props.hasSurvey || list.length < 2) return next(e)
536    const isFinished = list.every(task => task.state === 'done' || task.state === 'error')
537    const at = await read($, now)
538    const { Box, Text } = $.ui.resolve(e)
539    const n = list.length
540    const index = Math.max(
541      0,
542      list.findIndex(task => task.state === 'running') >= 0
543        ? list.findIndex(task => task.state === 'running')
544        : list.findIndex(task => task.state !== 'done'),
545    )
546    const head = list[index]
547    const done = list.filter(task => task.state === 'done').length
548    const percent = Math.round((done / n) * 100)
549    const Svg = e.surface === 'terminal' ? undefined : $.ui.resolve(e).Svg
550    const cell = Math.max(2, Math.floor((e.props.bodyColumns + 1) / n) - 1)
551    const bar =
552      !Svg ? (
553        <Box flexDirection="row">
554          {list.map(task => (
555            <Text color={THEME[task.state]}>{`${'━'.repeat(cell)} `}</Text>
556          ))}
557        </Box>
558      ) : (
559        <Svg source={planSvg(list)} alt={head ? `Etapa atual: ${head.subject}` : 'Plano em andamento'} height={12} />
560      )
561
562    return (
563      <Box key="plano" flexDirection="column">
564        {bar}
565        {isFinished ? (
566          <Text dimColor>{`Plano concluído · ${done}/${n} etapas · ${percent}%`}</Text>
567        ) : head && (
568          <Text dimColor>{`Etapa ${index + 1}/${n} · ${percent}% · ${head.state === 'running' && head.active ? head.active : head.subject} — ${TASK_LABEL[head.state]}`}</Text>
569        )}
570        <Box flexDirection="column" display="none" hover={{ display: 'flex' }}>
571          {list.map((task, i) => (
572            <Text key={`etapa-${i}`} color={THEME[task.state]}>{`● ${stepLine(task, i, n, at)}`}</Text>
573          ))}
574        </Box>
575      </Box>
576    )
577  })
578}
579
types/index.d.ts 38 lines
1export type AgentState = 'running' | 'done' | 'error'
2
3export type AgentRow = {
4  id: string
5  toolUseId: string
6  description: string
7  summary: string
8  type: string
9  model: string
10  state: AgentState
11  startedAt: number
12  endedAt?: number
13  tokens: number
14  result?: string
15  isExpanded?: boolean
16}
17
18export type TaskState = 'pending' | 'running' | 'done' | 'error'
19
20export type TaskDot = {
21  id: string
22  subject: string
23  active?: string
24  state: TaskState
25  startedAt?: number
26  endedAt?: number
27}
28
29declare module 'claude-code' {
30  interface PluginState {
31    'agentes-pixel': {
32      agents: AgentRow[]
33      tasks: Record<string, TaskDot[]>
34      now: number
35    }
36  }
37}
38