/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.

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.
/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.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.Cada subagente consume uso de tu plan. Elige el equipo según el trabajo: cinco áreas distintas, cinco agentes.
/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.
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.
claude plugin validate .
claude plugin test .
claude --plugin-dir .hooks/register.tsx 191 lines1import { 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}
191types/index.d.ts 21 lines1export 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