SLOPSHOPPER

agent-radar

One live line above the prompt per running subagent: time, tool count and what it's doing. /radar shows every agent and its messages.

newpanebandguardcommandtoast
v0.1.3MITupdated 2026-10-20Covre81/claude-code-mods/agent-radar
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agent-radar
│ ┃ Agent Radar ✕ › fix the failing auth test and add an audit log call │ ┃ 0 running · 0 finished │ ┃ No subagents yet this session. ⏺ Read(src/auth.ts) │ ┃ [ Clear finished ] [ Close ] ⎿ 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 │ │ › /radar │ ⎿ agent-radar: 0 running, 0 finished │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Agent Radar
0 running · 0 finished No subagents yet this session. [ Clear finished ] [ Close ]
README

claude-code-mods

Function-hook mods for Claude Code: shell safety, secret guarding, session telemetry, context panels, and workflow tools. Each mod is an isolated plugin with its own tests, and you enable only the ones you want.

Mods

Security

ModDescription
guardaIntercepts risky Bash commands (rm -rf, git reset --hard, force push, new dependencies) and asks for confirmation explaining the impact.
lixeiraSends rm / Remove-Item targets to the Windows Recycle Bin instead of deleting them permanently.
cofreKeeps API keys out of the conversation: /chave stores them in .env, secret file reads display only variable names, outputs are masked, and pasted keys are quarantined.
atomic-commit-guardBlocks commits that mix top-level directories, violate Conventional Commits, or lack a Co-Authored-By trailer.
universal-audit-logA hook on * that logs every event (tool calls, prompts, turns, fs/http/process calls from other plugins) with secret redaction.

Session & Context

ModDescription
context-monitorContext weather and token usage above the prompt, replay and activity panels, plain-language explanation before each edit, and ai-memory notes.
cache-frioConfirms before submitting a prompt to a large conversation whose prompt cache has gone cold.
session-telemetryTimestamp on each response, TTFT and tok/s per step, and visual notice whenever the responding model switches.
session-time-machine/timemachine: renders the session as a timeline of prompts, tool calls, and turn completions.
turn-inspectorSide pane displaying the session turn-by-turn: every model request and what it carried.
clean-viewCollapses each successful tool call into a single concise line (Edit file +12 −3); errors and in-flight calls retain the full block.
webfetch-cacheServes repeated WebFetch requests (identical URL and prompt) directly from the session cache.
reflexoDetects corrections and constraints in prompts, offering to save them as rules in CLAUDE.md.

Agents & Workflow

ModDescription
agent-flowLive diagram of agent flow: main loop context, subagent tree, and context passed/returned by each agent.
agent-radarOne live line per running subagent above the prompt; /radar opens a pane with messages from each.
delegation-dockStatus line indicator showing in-flight delegations (agy/codex/ollama) and alerting when one returns empty.
phase-trackerTracks and displays the current development phase (Spec → Red Test → Surgical Code → Verification).
next-promptsBand with 3 next-step suggestions generated by a free Ollama cloud model; click one to draft.
git-sidebarLazygit-style side pane: worktrees, local branches, branch switching, and worktree checkout.
wait-quizQuizzes a flashcard in a band above the prompt while Claude is working.
meus-modsCentral hub: quick-access bar above the prompt and /mods command displaying the status of each mod.

Installation

Requires Claude Code with function-hook mods support.

git clone https://github.com/Covre81/claude-code-mods.git ~/.claude/mods

Enable the mods by listing their absolute paths in CLAUDE_CODE_PLUGIN_DIRS inside ~/.claude/settings.json. On Windows, paths are separated by ;:

{
  "env": {
    "CLAUDE_CODE_PLUGIN_DIRS": "C:/Users/me/.claude/mods/guarda;C:/Users/me/.claude/mods/cofre;C:/Users/me/.claude/mods/session-telemetry"
  }
}

Order matters: the last plugin in the list sits closest to the engine. For instance, lixeira should come after guarda so guarda inspects the original rm -rf first.

Several mods include a command to toggle them on or off without restarting (/webfetch-cache on|off, /radar-toggle, /telemetria…); /mods displays the status of all mods.

Development

Each mod follows the same structure:

<mod>/
  .claude-plugin/plugin.json   manifest
  .claude-plugin/types/        engine types (git-ignored, see below)
  hooks/hooks.json             hooks registered by the mod
  hooks/register.ts(x)         code
  hooks/*.test.ts(x)           tests

Engine types are not committed to the repository. To typecheck and test a mod, copy .claude-plugin/types from an existing mod or generate them with your Claude Code instance.

Tests run directly within Claude Code, not via vitest/jest:

claude plugin test guarda               # single mod
for d in */hooks/hooks.json; do claude plugin test "${d%%/*}"; done   # all mods

Commits follow Conventional Commits, scoped to one mod per commit (enforced by atomic-commit-guard).

Credits

Six mods (agent-flow, git-sidebar, session-time-machine, turn-inspector, universal-audit-log, webfetch-cache) originate from davila7/claude-code-templates, and agent-radar originates from hamzafer/claude-code-mods, both under the MIT License and adapted here. Copyright notices are preserved in THIRD_PARTY_NOTICES.md.

License

MIT

Source 3 files
hooks/register.tsx 297 lines
1// Agent Radar: what are my subagents cooking?
2//   The band above the prompt has one live line per running subagent: status, time, tool
3//   count and what it is doing right now; a finished one shows a check for 30 s. A toast
4//   says when each finishes. /radar opens a pane with every agent and its messages.
5import { atom, read, update } from 'claude-code'
6import type { EngineInterface, Register } from 'claude-code'
7
8import type { RadarAgent } from '../types'
9import { bandsFile, parseOrder, stackBands } from './bands'
10
11const PANE = 'agent-radar'
12const SHOW_DONE_MS = 30_000 // a finished agent stays in the band this long
13const BAND_ROWS = 4
14
15// Held by the host, so the radar survives a hot reload of this file.
16const agents = atom({ plugin: 'agent-radar', key: 'agents' } as const, [] as RadarAgent[])
17const selected = atom({ plugin: 'agent-radar', key: 'selected' } as const, null as string | null)
18const now = atom({ plugin: 'agent-radar', key: 'now' } as const, 0)
19const isEnabledRef = atom({ plugin: 'agent-radar', key: 'isEnabled' } as const, true)
20
21let bandOrder: Promise<string[]> | undefined // read once per session: a new session applies an edit
22
23export const register: Register = on => {
24  on('session.start', async ($, e, next) => {
25    bandOrder = undefined // a new session reads the order file again
26    // Published so the meus-mods panel reads the state, never a copied default (the toggle is per session).
27    await update($, isEnabledRef, () => isEnabledRef.initial)
28    const r = await next(e)
29    await $.command.register({ name: 'radar', description: 'Show what each subagent is doing, in the Agent Radar pane' }).catch(() => {}) // a name Claude Code already has is refused: start anyway
30    await $.command.register({ name: 'radar-toggle', description: 'Toggle Agent Radar on/off' }).catch(() => {})
31
32    // A clock for the elapsed times, ticking only while something runs.
33    $.clock.every(1000, () => {
34      void (async () => {
35        // A few seconds past the 30 s too, so the redraw that drops a finished line happens.
36        if ((await read($, agents)).some(a => isShown(a, Date.now() - 3_000))) await update($, now, () => Date.now())
37      })().catch(() => {})
38    })
39    return r
40  })
41
42  on('agent.spawn', async ($, e, next) => {
43    const r = await next(e)
44    if (r.agentId) {
45      const id = r.agentId
46      const one: RadarAgent = { id, description: e.description || e.name || e.subagentType, type: e.subagentType, model: r.model, tokens: 0, status: 'running', startedAt: Date.now(), tools: 0, last: 'starting' }
47      await update($, agents, list => [one, ...list.filter(a => a.id !== id)].slice(0, 30))
48    }
49    return r
50  })
51
52  on('tool.call', async ($, e, next) => {
53    if (!e.agentId) return next(e)
54    const id = e.agentId
55    const line = describe(e as unknown as Record<string, unknown>)
56    // Only a TodoWrite changes the progress here; any other call leaves it as it was.
57    const progress = e.tool === 'TodoWrite' ? todoProgress(e.todos) : undefined
58    const bump = (a: RadarAgent): RadarAgent => ({ ...a, tools: a.tools + 1, last: line, progress: progress ?? a.progress })
59    if (e.tool !== 'TaskCreate' && e.tool !== 'TaskUpdate') {
60      // fail open on the radar's write only: a catch around the handler would rerun the tool if next threw
61      await update($, agents, list => list.map(a => (a.id === id ? bump(a) : a))).catch(() => {})
62      return next(e)
63    }
64    // TaskCreate's id exists only in its result, so the Task tools count once the call has answered, in one update.
65    const r = await next(e)
66    const change = taskChange(e as unknown as Record<string, unknown>, r)
67    await update($, agents, list => list.map(a => (a.id === id ? (change ? withTask(bump(a), change) : bump(a)) : a))).catch(() => {}) // never lose the tool's result over the radar
68    return r
69  })
70
71  on('turn.step', async function* ($, e, next) {
72    const r = yield* next(e) // usage rides on the step's result, once the response is whole
73    if (e.agentId && r.usage) {
74      const id = e.agentId
75      const used = sumTokens(r.usage)
76      await update($, agents, list => list.map(a => (a.id === id ? { ...a, tokens: a.tokens + used } : a)))
77    }
78    return r
79  }).catch(async function* ($, e, next) {
80    return yield* next(e) // fail open: a radar error never breaks the step
81  })
82
83  on('turn.complete', async ($, e, next) => {
84    const r = await next(e)
85    if (e.agentId) {
86      const id = e.agentId
87      const failed = e.reason === 'error' || e.reason === 'aborted'
88      if (e.usage) {
89        // The turn's usage already sums its steps, so adding it to the step counts would double: take the larger.
90        const total = sumTokens(e.usage)
91        await update($, agents, list => list.map(a => (a.id === id ? { ...a, tokens: Math.max(a.tokens, total) } : a)))
92      }
93      const list = await update($, agents, all =>
94        all.map(a => (a.id === id ? { ...a, status: failed ? ('failed' as const) : ('done' as const), endedAt: Date.now(), last: failed ? `stopped: ${e.reason}` : 'finished' } : a)),
95      )
96      const one = list.find(a => a.id === id)
97      if (one) $.ui.toast(`${failed ? '✗' : '✓'} ${one.description} ${failed ? 'failed' : 'done'} (${elapsed(one)} · ${one.tools} tools)`)
98    }
99    return r
100  }).catch(($, e, next) => next(e)) // fail open: a radar error never breaks the turn
101
102  on('command.run', { command: 'radar' }, async $ => {
103    if (!(await read($, isEnabledRef).catch(() => true))) return { text: 'Agent Radar está desativado. Use /radar-toggle para ativar.' }
104    await adopt($)
105    await update($, selected, () => null)
106    await $.ui.open({ id: PANE, title: 'Agent Radar', focus: true })
107    const list = await read($, agents)
108    const running = list.filter(a => a.status === 'running').length
109    return { text: `${running} running, ${list.length - running} finished` }
110  })
111
112  on('command.run', { command: 'radar-toggle' }, async $ => {
113    const state = await update($, isEnabledRef, v => !v)
114    return { text: `agent-radar ${state ? 'ativado' : 'desativado'}.` }
115  })
116
117  // The band: one live line per running agent, and a ✓ for 30 s once one finishes.
118  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
119    if (!(await read($, isEnabledRef).catch(() => true))) return next(e)
120    const rest = await next(e) // what other mods and Claude Code draw here stays
121    await read($, now) // subscribes the band to the clock
122    const shown = (await read($, agents)).filter(a => isShown(a))
123    if (e.props.hasSurvey || shown.length === 0) return rest
124    const { Box, Text } = $.ui.resolve(e)
125    const mine = (
126      <Box flexDirection="column">
127        {shown.slice(0, BAND_ROWS).map(a => {
128          const m = meta(a)
129          return (
130          <Text key={a.id} wrap="truncate-end">
131            <Text color={color(a)} bold>{` ${icon(a)} ${a.description}`}</Text>
132            <Text dimColor>{`  ${elapsed(a)} · ${a.tools} tools`}</Text>
133            {a.status === 'running' && <Text>{` · ${a.last}`}</Text>}
134            {a.status === 'failed' && <Text color="red">{` · ${a.last}`}</Text>}
135            {m && <Text dimColor>{` · ${m}`}</Text>}
136            {a.progress ? <Text>{` · ${a.progress}`}</Text> : null}
137          </Text>
138          )
139        })}
140        {shown.length > BAND_ROWS && <Text dimColor>{`   +${shown.length - BAND_ROWS} more · /radar`}</Text>}
141      </Box>
142    )
143    return stackBands(rest, 'agent-radar', mine, 'above', await (bandOrder ??= readBandOrder($).catch(() => []))) // above what other mods draw, unless the band order says otherwise
144  })
145
146  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
147    const { Box, Text, Button } = $.ui.resolve(e)
148    await read($, now) // subscribes the pane to the clock
149    const list = await read($, agents)
150    const pick = await read($, selected)
151    const one = list.find(a => a.id === pick)
152
153    if (one) {
154      const found = await $.session.messages({ agentId: one.id })
155      const messages = 'deny' in found ? [] : found.slice(-6)
156      return (
157        <Box flexDirection="column">
158          <Text>
159            <Text color={color(one)} bold>{`${icon(one)} ${one.description}`}</Text>
160            <Text dimColor>{`  ${one.type} · ${elapsed(one)} · ${one.tools} tools`}</Text>
161          </Text>
162          <Text dimColor wrap="truncate-end">{`now: ${one.last}`}</Text>
163          <Text dimColor>{'─'.repeat(Math.max(10, e.props.bodyColumns - 2))}</Text>
164          {messages.length === 0 && <Text dimColor>No messages yet.</Text>}
165          {messages.map(m => (
166            <Text wrap="truncate-end">
167              <Text color={m.role === 'assistant' ? 'cyan' : undefined} bold>{m.role === 'assistant' ? 'agent: ' : 'task:  '}</Text>
168              <Text>{m.text.replace(/\s+/g, ' ').slice(0, 400) || '(tool calls)'}</Text>
169            </Text>
170          ))}
171          <Box flexDirection="row" gap={1}>
172            <Button key="back" label="Back" hotkey="b" variant="primary" onPress={() => update($, selected, () => null)} />
173            <Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
174          </Box>
175        </Box>
176      )
177    }
178
179    const running = list.filter(a => a.status === 'running').length
180    return (
181      <Box flexDirection="column">
182        <Text bold>{`${running} running · ${list.length - running} finished`}</Text>
183        {list.length === 0 && <Text dimColor>No subagents yet this session.</Text>}
184        {list.slice(0, 9).map((a, i) => (
185          <Button
186            key={`agent-${a.id}`}
187            plain
188            hotkey={String(i + 1)}
189            dimColor={a.status !== 'running'}
190            label={`${icon(a)} ${a.description} · ${elapsed(a)} · ${a.tools} tools${a.status === 'done' ? '' : ` · ${a.last}`}`}
191            onPress={() => update($, selected, () => a.id)}
192          />
193        ))}
194        {list.length > 9 && <Text dimColor>{`… ${list.length - 9} older`}</Text>}
195        <Box flexDirection="row" gap={1}>
196          <Button key="clear" label="Clear finished" hotkey="c" onPress={() => update($, agents, all => all.filter(x => x.status === 'running'))} />
197          <Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
198        </Box>
199      </Box>
200    )
201  })
202}
203
204// The band order file, as the claude-code-mods checklist saves it; [] on any error.
205async function readBandOrder($: EngineInterface): Promise<string[]> {
206  const file = bandsFile(await $.env.get('CLAUDE_CONFIG_DIR'), await $.env.get('HOME'))
207  return file && (await $.fs.exists(file)) ? parseOrder(await $.fs.read(file)) : []
208}
209
210// Agents that started before this mod loaded: take them from the engine's own list.
211async function adopt($: EngineInterface) {
212  const known = new Set((await read($, agents)).map(a => a.id))
213  const fresh = (await $.agent.list())
214    .filter(a => !known.has(a.id))
215    .map((a): RadarAgent => ({
216      id: a.id,
217      description: a.description,
218      type: a.type,
219      tokens: 0,
220      status: a.status === 'running' ? 'running' : a.status === 'completed' ? 'done' : 'failed',
221      startedAt: Date.now(),
222      tools: 0,
223      last: a.status === 'running' ? 'running (started before the radar)' : a.status,
224    }))
225  if (fresh.length > 0) await update($, agents, list => [...list, ...fresh])
226}
227
228// Running, or finished in the last 30 s.
229export function isShown(a: RadarAgent, at = Date.now()) {
230  return a.status === 'running' || (a.endedAt !== undefined && at - a.endedAt < SHOW_DONE_MS)
231}
232
233function icon(a: RadarAgent) {
234  return a.status === 'running' ? '●' : a.status === 'done' ? '✓' : '✗'
235}
236
237function color(a: RadarAgent) {
238  return a.status === 'running' ? 'yellow' : a.status === 'done' ? 'green' : 'red'
239}
240
241export function elapsed(a: Pick<RadarAgent, 'startedAt' | 'endedAt'>, at = Date.now()) {
242  const s = Math.max(0, Math.round(((a.endedAt ?? at) - a.startedAt) / 1000))
243  return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
244}
245
246// Model and tokens, each only once the engine has reported it.
247function meta(a: RadarAgent) {
248  return [a.model, a.tokens > 0 ? formatTokens(a.tokens) : undefined].filter(Boolean).join(' · ')
249}
250
251// Tokens the step or turn processed fresh: uncached input, cache writes and output. Cache reads are left out:
252// every step re-reads the whole cached context, so summing them counts that context once per step.
253function sumTokens(u: { input_tokens?: number | null; output_tokens?: number | null; cache_creation_input_tokens?: number | null }) {
254  return (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0)
255}
256
257// "3/7 tarefas" from a TodoWrite list; undefined when the input is not a list.
258function todoProgress(todos: unknown) {
259  if (!Array.isArray(todos)) return undefined
260  return `${todos.filter(t => t?.status === 'completed').length}/${todos.length} tarefas`
261}
262
263// What a TaskCreate or TaskUpdate did to the agent's task list: [task id, status]; undefined for any other call.
264function taskChange(e: Record<string, unknown>, r: { deny?: string; isError?: boolean; result?: unknown }): [string, string] | undefined {
265  if (r.deny !== undefined || r.isError) return undefined
266  const created = (r.result as { task?: { id?: unknown } } | undefined)?.task?.id
267  if (e.tool === 'TaskCreate' && typeof created === 'string') return [created, 'pending']
268  if (e.tool === 'TaskUpdate' && typeof e.taskId === 'string' && typeof e.status === 'string') return [e.taskId, e.status]
269  return undefined
270}
271
272function withTask(a: RadarAgent, [taskId, status]: [string, string]): RadarAgent {
273  const { [taskId]: _, ...rest } = a.tasks ?? {}
274  const tasks = status === 'deleted' ? rest : { ...rest, [taskId]: status }
275  const all = Object.values(tasks)
276  return { ...a, tasks, progress: all.length === 0 ? undefined : `${all.filter(s => s === 'completed').length}/${all.length} tarefas` }
277}
278
279function formatTokens(n: number) {
280  if (n < 1000) return `${n} tok`
281  return `${(Math.round(n / 100) / 10).toString()}k tok` // 1500 -> 1.5k, 2000 -> 2k, 42000 -> 42k
282}
283
284// A short label for one tool call: what a person would say the agent is doing.
285export function describe(e: Record<string, unknown>): string {
286  const tool = String(e.tool)
287  const s = (k: string) => (typeof e[k] === 'string' ? (e[k] as string) : '')
288  const file = (p: string) => p.split('/').slice(-2).join('/')
289  if (tool === 'Bash') return `running ${s('description') || s('command').slice(0, 50)}`
290  if (tool === 'Edit' || tool === 'Write') return `editing ${file(s('file_path'))}`
291  if (tool === 'Read') return `reading ${file(s('file_path'))}`
292  if (tool === 'Grep' || tool === 'Glob') return `searching ${s('pattern').slice(0, 30)}`
293  if (tool.includes('playwright')) return `browser: ${tool.split('__').pop()?.replace(/^browser_/, '')}`
294  if (tool.startsWith('mcp__')) return `using ${tool.split('__').slice(1).join(' ')}`
295  return `using ${tool}`
296}
297
hooks/bands.ts 73 lines
1// Band order: every band mod ships this same file (canonical copy: shared/bands.ts in the repo).
2//   The order file, <configDir>/claude-code-mods/bands.json, lists mod names top to bottom
3//   (the last sits right above the prompt). Each band mod wraps its band in a Box keyed
4//   `band:<name>` and returns one column keyed `bands`; the next band mod unpacks that
5//   column, adds its own band and sorts again, so the last one to draw sorts them all.
6//   Edit shared/bands.ts, then copy it over every mod's hooks/bands.ts (a repo test checks).
7//   Reading the file needs $, which Claude Code follows only within one file, so each mod
8//   reads it in its own register file with bandsFile and parseOrder, once per session.
9import type { RenderElement, RenderNode } from 'claude-code'
10
11const COLUMN = 'bands'
12const PREFIX = 'band:'
13
14// Where the checklist saves the order: CLAUDE_CONFIG_DIR, or ~/.claude. Undefined with neither.
15export function bandsFile(configDir: string | undefined, home: string | undefined): string | undefined {
16  const dir = configDir ? configDir.replace(/\/+$/, '') : home ? `${home}/.claude` : ''
17  return dir ? `${dir}/claude-code-mods/bands.json` : undefined
18}
19
20// The names in an order file; [] when it is no JSON or not { order: string[] }.
21export function parseOrder(text: string): string[] {
22  try {
23    const order = (JSON.parse(text) as { order?: unknown } | null)?.order
24    return Array.isArray(order) && order.every(n => typeof n === 'string') ? order : []
25  } catch {
26    return []
27  }
28}
29
30/**
31 * One column of bands: `mine` wrapped in a Box keyed `band:<name>`, placed
32 * `at` (above or below) what `next(e)` returned, then sorted by `order`.
33 * With an order, items that are no band come first, then bands not in the
34 * order (in their current order), then the listed ones in file order. With
35 * no order nothing moves, so the screen looks as it did before.
36 */
37export function stackBands(rest: RenderElement | null | undefined, name: string, mine: RenderNode, at: 'above' | 'below', order: string[]): RenderElement {
38  const own: RenderElement = { type: 'Box', props: { key: PREFIX + name, flexDirection: 'column' }, children: [mine] }
39  const others = unpack(rest)
40  const items = at === 'above' ? [own, ...others] : [...others, own]
41  return { type: 'Box', props: { key: COLUMN, flexDirection: 'column' }, children: order.length ? sorted(items, order) : items }
42}
43
44// Another band mod's column gives up its items; anything else is one item.
45function unpack(rest: RenderElement | null | undefined): RenderNode[] {
46  if (!rest) return []
47  if (!isColumn(rest)) return [rest]
48  const kids = rest.children ?? []
49  const owner = rest.part?.plugin
50  // The engine stamped the column with its owner: that stamp belongs on the owner's own band.
51  return kids.map(k => (owner && typeof k !== 'string' && bandName(k) === owner && !('part' in k && k.part) ? { ...k, part: rest.part } : k))
52}
53
54function isColumn(el: RenderElement): el is RenderElement & { type: 'Box' } {
55  return el.type === 'Box' && el.props?.key === COLUMN
56}
57
58function bandName(node: RenderNode): string | undefined {
59  if (typeof node === 'string' || node.type !== 'Box') return undefined
60  const key = node.props?.key
61  return typeof key === 'string' && key.startsWith(PREFIX) ? key.slice(PREFIX.length) : undefined
62}
63
64function sorted(items: RenderNode[], order: string[]): RenderNode[] {
65  const rank = (n: RenderNode) => {
66    const name = bandName(n)
67    if (name === undefined) return -2 // not a band: first
68    const i = order.indexOf(name)
69    return i < 0 ? -1 : i // a band not in the order: on top of the listed ones
70  }
71  return items.map((n, i) => ({ n, i, r: rank(n) })).sort((a, b) => a.r - b.r || a.i - b.i).map(x => x.n)
72}
73
types/index.d.ts 21 lines
1export type RadarAgent = {
2  id: string
3  description: string
4  type: string
5  model?: string // left out when the engine does not report it
6  tokens: number
7  progress?: string
8  tasks?: Record<string, string> // TaskCreate/TaskUpdate: task id -> status
9  status: 'running' | 'done' | 'failed'
10  startedAt: number
11  endedAt?: number
12  tools: number
13  last: string
14}
15
16declare module 'claude-code' {
17  interface PluginState {
18    'agent-radar': { agents: RadarAgent[]; selected: string | null; now: number; isEnabled: boolean }
19  }
20}
21