SLOPSHOPPER

agent-dock

/dock abre un panel para elegir cuántos subagentes usar en el siguiente encargo y seguir una tarjeta por agente: tarea, tipo, estado y tiempo.

panecommandtoastpromptagents
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agent-dock
│ ┃ Agent Dock ✕ › fix the failing auth test and add an audit log call │ ┃ Agentes para el siguiente encargo: │ ┃ [ 1 ][ 2 ][ [3] ][ 4 ][ 5 ][ 6 ][ ⏺ Read(src/auth.ts) │ ┃ Pulsa un número (o /dock N) y el siguiente ⎿ Read 6 lines │ ┃ mensaje se repartirá entre esos agentes. ⏺ Update(src/auth.ts) │ ┃ ⎿ Added 2 lines, removed 1 line │ ┃ Tarjetas · 0 trabajando · 0 terminadas ⏺ Bash(bun test) │ ┃ Aún no hay agentes en esta sesión. ⎿ 3 pass, 1 fail │ ┃ [ Limpiar terminadas ] [ Cerrar ] │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /dock │ ⎿ agent-dock: Agent Dock abierto. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Agent Dock
Agentes para el siguiente encargo: [ 1 ][ 2 ][ [3] ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ] Pulsa un número (o /dock N) y el siguiente mensaje se repartirá entre esos agentes. Tarjetas · 0 trabajando · 0 terminadas Aún no hay agentes en esta sesión. [ Limpiar terminadas ] [ Cerrar ]
README

agent-dock

Un mod de Claude Code: /dock abre un panel para elegir cuántos subagentes usar en el siguiente encargo y seguir una tarjeta por agente.

  • Equipo: botones 1 a 8 (teclas rápidas con el panel enfocado) o /dock 5. El siguiente mensaje que envíes lleva, como contexto para el modelo, la instrucción de repartir el trabajo entre ese número de subagentes y de integrar sus resultados al final. Se aplica una sola vez por elección.
  • Tarjetas: cada subagente que arranca (agent.spawn) aparece con su tarea, su tipo, su estado (● trabajando, ✓ terminado, ✗ falló) y el tiempo transcurrido. El estado se actualiza cuando termina el turno del subagente y, al cerrar el turno principal, con lo que $.agent.list() sepa de cada uno.
  • Limpiar terminadas deja solo las activas; Cerrar cierra el panel. El panel se abre solo cuando arranca un agente.

Cada subagente consume uso de tu plan. Elige el equipo según el trabajo: cinco áreas distintas, cinco agentes.

Instalar

/plugin install agent-dock --marketplace alvaroruizr2024-cpu/www.innovaqsolution.com

O desde la terminal: claude plugin install agent-dock@innovaq-mods tras claude plugin marketplace add alvaroruizr2024-cpu/www.innovaqsolution.com.

Cómo funciona

hooks/register.tsx registra /dock en session.start, lo atiende en command.run, añade el contexto en prompt.submit mientras el dock está armado, registra tarjetas en agent.spawn, las cierra en turn.complete y dibuja el panel con ui.render sobre Pane. El estado vive en $.state bajo agent-dock (crew, isArmed, cards), declarado en types/index.d.ts.

Desarrollar

claude plugin validate .
claude plugin test .
claude --plugin-dir .
Source 2 files
hooks/register.tsx 191 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { AgentCard } from '../types'
5
6// Agent Dock: `/dock` abre un panel donde eliges cuántos subagentes quieres
7// para el siguiente encargo y sigues una tarjeta por agente mientras trabajan.
8
9const PANE = 'agent-dock'
10const MAX_CARDS = 40
11const MIN_CREW = 1
12const MAX_CREW = 8
13
14const crew = atom({ plugin: 'agent-dock', key: 'crew' } as const, 3)
15const isArmed = atom({ plugin: 'agent-dock', key: 'isArmed' } as const, false)
16const cards = atom({ plugin: 'agent-dock', key: 'cards' } as const, [])
17
18const GLYPH: Record<AgentCard['status'], string> = { running: '●', done: '✓', failed: '✗' }
19const WORD: Record<AgentCard['status'], string> = { running: 'trabajando', done: 'terminado', failed: 'falló' }
20
21function elapsed(from: number, to: number): string {
22  const seconds = Math.max(0, Math.round((to - from) / 1000))
23  return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`
24}
25
26function brief(n: number): string {
27  return [
28    `Reparte este encargo entre ${n} subagentes que trabajen en paralelo.`,
29    'Divide el trabajo en partes claramente delimitadas, una por agente, con una descripción corta de cada tarea.',
30    'Cuando terminen, integra sus resultados en una sola entrega y señala cualquier conflicto entre ellos.',
31  ].join(' ')
32}
33
34export const register: Register = on => {
35  on('session.start', async ($, e, next) => {
36    await $.command.register({
37      name: 'dock',
38      description: 'Agent Dock: elige cuántos subagentes usar y sigue sus tarjetas en un panel',
39      argumentHint: '[1-8]',
40    })
41
42    return next(e)
43  })
44
45  on('command.run', { command: 'dock' }, async ($, e) => {
46    const n = Number.parseInt(e.args.trim(), 10)
47    let text = 'Agent Dock abierto.'
48    if (Number.isInteger(n) && n >= MIN_CREW && n <= MAX_CREW) {
49      await update($, crew, () => n)
50      await update($, isArmed, () => true)
51      text = `Agent Dock: el siguiente encargo se repartirá entre ${n} ${n === 1 ? 'agente' : 'agentes'}.`
52    }
53    await $.ui.open({ id: PANE, title: 'Agent Dock', focus: true })
54
55    return { text }
56  })
57
58  // El siguiente prompt lleva la instrucción de repartir el trabajo.
59  on('prompt.submit', async ($, e, next) => {
60    if (e.command !== undefined || e.isMeta || e.origin.kind !== 'composer' || !(await read($, isArmed))) {
61      return next(e)
62    }
63    const n = await read($, crew)
64    await update($, isArmed, () => false)
65    $.ui.toast(`Agent Dock: encargo repartido entre ${n} agentes`)
66
67    return next({ ...e, context: [...(e.context ?? []), brief(n)] })
68  })
69
70  // Cada subagente que arranca es una tarjeta.
71  on('agent.spawn', async ($, e, next) => {
72    const spawned = await next(e)
73    if (spawned.deny !== undefined || spawned.agentId === undefined) {
74      return spawned
75    }
76    const card: AgentCard = {
77      id: spawned.agentId,
78      description: e.description || e.prompt.slice(0, 60),
79      type: e.subagentType,
80      status: 'running',
81      startedAt: await $.clock.now(),
82    }
83    await update($, cards, list => [...list.filter(one => one.id !== card.id), card].slice(-MAX_CARDS))
84    void $.ui.open({ id: PANE, title: 'Agent Dock' })
85    $.ui.invalidate('ui.render')
86
87    return spawned
88  })
89
90  // El turno de un subagente termina: su tarjeta cambia de estado.
91  on('turn.complete', async ($, e, next) => {
92    if (e.agentId !== undefined) {
93      const status: AgentCard['status'] = e.reason === 'answer' ? 'done' : 'failed'
94      const endedAt = await $.clock.now()
95      await update($, cards, list =>
96        list.map(one => (one.id === e.agentId && one.status === 'running' ? { ...one, status, endedAt } : one)),
97      )
98      $.ui.invalidate('ui.render')
99    } else {
100      // Al cerrar el turno principal, lo que el motor sepa de cada agente manda.
101      const known = await $.agent.list()
102      const endedAt = await $.clock.now()
103      await update($, cards, list =>
104        list.map(one => {
105          const live = known.find(agent => agent.id === one.id)
106          if (one.status !== 'running' || live === undefined) {
107            return one
108          }
109          if (live.status === 'completed') {
110            return { ...one, status: 'done', endedAt }
111          }
112          if (live.status === 'failed' || live.status === 'killed') {
113            return { ...one, status: 'failed', endedAt }
114          }
115
116          return one
117        }),
118      )
119      $.ui.invalidate('ui.render')
120    }
121
122    return next(e)
123  })
124
125  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
126    const { Box, Button, Text } = $.ui.resolve(e)
127    const n = await read($, crew)
128    const armed = await read($, isArmed)
129    const list = await read($, cards)
130    const now = await $.clock.now()
131    const running = list.filter(card => card.status === 'running').length
132    const done = list.filter(card => card.status === 'done').length
133    const sizes = Array.from({ length: MAX_CREW }, (_, i) => i + MIN_CREW)
134
135    return (
136      <Box flexDirection="column">
137        <Text>Agentes para el siguiente encargo:</Text>
138        <Box flexWrap="wrap">
139          {sizes.map(size => (
140            <Button
141              key={`crew-${size}`}
142              label={size === n ? `[${size}]` : ` ${size} `}
143              hotkey={String(size)}
144              variant={size === n ? 'primary' : 'secondary'}
145              onPress={async () => {
146                await update($, crew, () => size)
147                await update($, isArmed, () => true)
148                $.ui.invalidate('ui.render')
149              }}
150            />
151          ))}
152        </Box>
153        <Text dimColor>
154          {armed
155            ? `Listo: tu próximo mensaje se repartirá entre ${n} ${n === 1 ? 'agente' : 'agentes'}.`
156            : 'Pulsa un número (o /dock N) y el siguiente mensaje se repartirá entre esos agentes.'}
157        </Text>
158        <Text> </Text>
159        <Text>
160          Tarjetas · {running} trabajando · {done} terminadas
161        </Text>
162        {list.length === 0 && <Text dimColor>Aún no hay agentes en esta sesión.</Text>}
163        {list.map(card => (
164          <Box key={card.id} flexDirection="column">
165            <Text dimColor={card.status !== 'running'}>
166              {GLYPH[card.status]} {card.description}
167            </Text>
168            <Text dimColor>
169              {'   '}
170              {card.type} · {WORD[card.status]} · {elapsed(card.startedAt, card.endedAt ?? now)}
171            </Text>
172          </Box>
173        ))}
174        <Box>
175          <Button
176            key="clear"
177            label="Limpiar terminadas"
178            hotkey="l"
179            onPress={async () => {
180              await update($, cards, list => list.filter(card => card.status === 'running'))
181              $.ui.invalidate('ui.render')
182            }}
183          />
184          <Text> </Text>
185          <Button key="close" label="Cerrar" hotkey="c" onPress={() => $.ui.close({ id: PANE })} />
186        </Box>
187      </Box>
188    )
189  })
190}
191
types/index.d.ts 21 lines
1export type CardStatus = 'running' | 'done' | 'failed'
2
3export type AgentCard = {
4  id: string
5  description: string
6  type: string
7  status: CardStatus
8  startedAt: number
9  endedAt?: number
10}
11
12declare module 'claude-code' {
13  interface PluginState {
14    'agent-dock': {
15      crew: number
16      isArmed: boolean
17      cards: AgentCard[]
18    }
19  }
20}
21