SLOPSHOPPER

agent-board

Claude Code skill that builds, debugs, audits and packages Claude Code mods from a plain-language description

newpanecommandtimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agent-board
│ ┃ Agents ✕ › fix the failing auth test and add an audit log call │ ┃ ◆ Agents ● all idle │ ┃ ⏺ Read(src/auth.ts) │ ┃ No agents in this session yet. ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ refreshes every 2 s · last 8:53:20 AM ⎿ 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 │ │ › /agents │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Agents
◆ Agents ● all idle No agents in this session yet. refreshes every 2 s · last 8:53:20 AM
README

agent-board

Leia em português

A panel listing the helper agents Claude started in this session and what each one is doing.

What is a mod?

Claude Code is Anthropic's assistant for working with code, in the terminal or in the Claude desktop app. A mod is a small add-on for it: it can add a panel, a bar above the box where you type, a new /command, or a rule that checks what Claude is about to do. This folder is one complete mod you can install and use as it is, or copy as a starting point.

What it does

For bigger tasks Claude can start helper agents (subagents) that work in parallel: one researches, another writes code, another reviews. Normally you only see their final reports. This mod shows a live board with every helper, its role and its status, so you know who is still working.

What you see

  • A panel titled "Agents" with one row per helper: name, role and status (starting, working, waiting, idle, completed, failed, stopped).
  • A summary at the top ("◌ 2 working" or "● all idle") and the time of the last update.

How to use it

  1. Type /agents and press Enter. The panel opens.
  2. Leave it open while Claude works; it updates every 2 seconds.
  3. Press Esc to go back to the prompt.

Keyboard shortcuts

KeyWhat it does
EscLeave the panel

Commands

TypeWhat it does
/agentsOpen the board

Settings

Claude Code asks for these when you enable the mod. To change them later, open /config and find the mod by name.

SettingWhat it meansDefault
language (Language)Language of the text the mod shows: en (English) or pt-BR (Brazilian Portuguese).en

Good to know

  • If Claude has not started any helper, the panel says so.
  • It pairs well with agent-dialogue, which shows what the helpers are saying to each other.

Install

You need Claude Code 2.1.287 or newer in the terminal, or 2.1.286 or newer in the desktop app.

  1. Copy this folder anywhere on your computer.
  2. Inside the copy, rename the folder _claude-plugin to .claude-plugin (with a dot at the start). It ships without the dot so the examples never load by accident.
  3. Start Claude Code with the mod:
claude --plugin-dir path/to/agent-board

To keep it installed for good, ask the claude-mod-forge skill to package it ("package this mod").

Check it

From the skill folder (skills/claude-mod-forge), this runs the manifest check, a strict type check and the 4 automated tests of this mod:

node scripts/check-mod.mjs examples/agent-board

The tests cover the mod's logic and what it draws in the terminal and the desktop app. How it looks in your own window is worth a quick look in a real session.

For developers: the examples table lists the hooks and API calls this mod demonstrates.

Source 3 files
hooks/register.tsx 96 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { AgentRow } from '../types'
5import { strings } from './i18n'
6
7// A live board of this session's subagents. Data source: $.agent.list(), polled every 2 s
8// (there is no push event for subagent status). Reads only: it never starts or stops an agent.
9const PANE = 'agent-board'
10const agents = atom({ plugin: 'agent-board', key: 'agents' } as const, [])
11const checkedAt = atom({ plugin: 'agent-board', key: 'checkedAt' } as const, 0)
12
13// Icon and color per status; the label comes from i18n.ts.
14const STATUS: Record<string, { icon: string; color: string }> = {
15  pending: { icon: '◌', color: 'warning' },
16  running: { icon: '◌', color: 'warning' },
17  waiting: { icon: '…', color: 'suggestion' },
18  idle: { icon: '●', color: 'subtle' },
19  completed: { icon: '✓', color: 'success' },
20  failed: { icon: '✗', color: 'error' },
21  killed: { icon: '✗', color: 'error' },
22}
23
24async function poll($: EngineInterface) {
25  try {
26    const list = await $.agent.list()
27    const rows: AgentRow[] = list.map(a => ({
28      id: a.id,
29      name: a.name ?? a.type,
30      role: a.description.slice(0, 80),
31      status: a.status,
32    }))
33    const at = await $.clock.now()
34    await update($, agents, () => rows)
35    await update($, checkedAt, () => at)
36  } catch {
37    // keep the last list
38  }
39}
40
41export const register: Register = (on, options) => {
42  const t = strings(options.language)
43  const label = (status: string) => t.status[status] ?? status
44
45  on('session.start', async ($, e, next) => {
46    await poll($)
47    $.clock.every(2000, () => poll($))
48    try {
49      await $.command.register({ name: 'agents', description: t.commandDescription, immediate: true })
50    } catch {
51      // name taken
52    }
53    return next(e)
54  })
55
56  on('command.run', { command: 'agents' }, async $ => {
57    await poll($)
58    const opened = await $.ui.open({ id: PANE, title: t.paneTitle, focus: true })
59    if (opened.isPlaced) return {}
60    // Nothing draws here (VS Code chat, claude -p): the same list as text.
61    const rows = await read($, agents)
62    return { text: rows.length === 0 ? t.noAgents : rows.map(r => `${r.name}: ${label(r.status)}`).join('\n') }
63  })
64
65  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
66    const { Box, Text } = $.ui.resolve(e)
67    const rows = await read($, agents)
68    const at = await read($, checkedAt)
69    const working = rows.filter(r => r.status === 'running' || r.status === 'pending').length
70
71    return (
72      <Box flexDirection="column" paddingX={1} gap={1}>
73        <Box justifyContent="space-between">
74          <Text bold color="claude">{t.heading}</Text>
75          <Text color={working > 0 ? 'warning' : 'success'}>{working > 0 ? t.working(working) : t.allIdle}</Text>
76        </Box>
77        <Box flexDirection="column">
78          {rows.length === 0 && <Text dimColor>{t.noAgentsYet}</Text>}
79          {rows.map(r => {
80            const s = STATUS[r.status] ?? { icon: '?', color: 'text' }
81            return (
82              <Box key={`agent-${r.id}`} gap={1}>
83                <Text color={s.color}>{s.icon}</Text>
84                <Box width={14}><Text bold wrap="truncate-end">{r.name}</Text></Box>
85                <Box width={12}><Text color={s.color}>{label(r.status)}</Text></Box>
86                <Box flexGrow={1}><Text dimColor wrap="truncate-end">{r.role}</Text></Box>
87              </Box>
88            )
89          })}
90        </Box>
91        <Text dimColor>{at === 0 ? t.refresh : t.refreshLast(new Date(at).toLocaleTimeString(t.locale))}</Text>
92      </Box>
93    )
94  })
95}
96
hooks/i18n.ts 56 lines
1// The text this mod shows, per language. Pure: no $ calls, so tests can cover it directly.
2// Anything other than a known language falls back to English.
3
4export type Language = 'en' | 'pt-BR'
5
6const en = {
7  locale: 'en-US',
8  status: {
9    pending: 'starting',
10    running: 'working',
11    waiting: 'waiting',
12    idle: 'idle',
13    completed: 'done',
14    failed: 'failed',
15    killed: 'stopped',
16  } as Record<string, string>,
17  commandDescription: 'Show the agents of this session and the status of each',
18  paneTitle: 'Agents',
19  heading: '◆ Agents',
20  working: (n: number) => `◌ ${n} working`,
21  allIdle: '● all idle',
22  noAgents: 'No agents in this session.',
23  noAgentsYet: 'No agents in this session yet.',
24  refresh: 'refreshes every 2 s',
25  refreshLast: (time: string) => `refreshes every 2 s · last ${time}`,
26}
27
28const ptBR: typeof en = {
29  locale: 'pt-BR',
30  status: {
31    pending: 'iniciando',
32    running: 'trabalhando',
33    waiting: 'aguardando',
34    idle: 'parado',
35    completed: 'concluído',
36    failed: 'falhou',
37    killed: 'interrompido',
38  },
39  commandDescription: 'Mostra os agentes desta sessão e o status de cada um',
40  paneTitle: 'Agentes',
41  heading: '◆ Agentes',
42  working: (n: number) => `◌ ${n} trabalhando`,
43  allIdle: '● tudo parado',
44  noAgents: 'Nenhum agente nesta sessão.',
45  noAgentsYet: 'Nenhum agente nesta sessão ainda.',
46  refresh: 'atualiza a cada 2 s',
47  refreshLast: (time: string) => `atualiza a cada 2 s · última ${time}`,
48}
49
50export type Strings = typeof en
51const TABLE: Record<Language, Strings> = { en, 'pt-BR': ptBR }
52
53export function strings(language: unknown): Strings {
54  return language === 'pt-BR' ? TABLE['pt-BR'] : TABLE.en
55}
56
types/index.d.ts 8 lines
1export type AgentRow = { id: string; name: string; role: string; status: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'agent-board': { agents: AgentRow[]; checkedAt: number }
6  }
7}
8