A band above the prompt with each running subagent's type, model, effort, steps, context, advisor calls and estimated cost; a /fleet pane, /steer…

A Claude Code plugin marketplace from Darling Data.
/plugin marketplace add erikdarlingdata/claude-plugins
sqlserver-query-plansTeaches Claude to read a SQL Server execution plan and say what is actually slow, and why.
/plugin install sqlserver-query-plans@erikdarling
Point Claude at a .sqlplan file and ask. The skill is model-invoked — you do not need to call it explicitly.
The hard part of plan analysis is not spotting operators. It is knowing which numbers mean what they appear to mean. This plugin is built mostly out of the conclusions that sound authoritative and are wrong:
ActualElapsedms ranks them by depth and always crowns the root node. Batch mode reports standalone times. Exchange operators report times that are close to meaningless.EstimateRows is per-execution; ActualRows is a total. Without dividing by ActualExecutions, the inner side of every nested loop looks catastrophically underestimated when it may have estimated perfectly.Impact figure is a percentage of an estimated cost.It also ships scripts/extract.py, which flattens a .sqlplan into a compact digest. This is not a convenience:
.sqlplan files are UTF-16, so grep silently matches nothing and reports no error. A negative result from grep on a plan file is worthless.encoding="utf-16", because they were opened and re-saved. Strict XML parsers reject them.The extractor handles the encoding, computes correct self-time attribution (subtracting children in row mode, within a thread rather than across threads in parallel plans, and not at all in batch mode), normalizes cardinality per execution, and recognizes the optimizer's default-guess selectivity fingerprints. --node N drills into a single operator; --sql recovers full statement text.
Requires Python 3 (standard library only). Without it, the skill degrades to a documented grep-based fallback and says plainly what it cannot determine.
This plugin also works in GitHub Copilot CLI, which reads the same SKILL.md and plugin.json format.
copilot plugin marketplace add erikdarlingdata/claude-plugins
copilot plugin install sqlserver-query-plans@erikdarling
Or add just the skill, without the marketplace:
/skills add ./plugins/sqlserver-query-plans/skills/query-plan-analysis
As in Claude Code, the skill is model-invoked: point Copilot at a .sqlplan and ask.
Small hook modules for people who run several agents at once. Each one is a plugin of its own. Install only the ones you want, and set their options in /config (or under pluginConfigs in settings.json). They need a Claude Code version that loads plugin hook modules.
subagent-fenceStops the mistakes an unattended agent makes that cannot be taken back.
/plugin install subagent-fence@erikdarling
For the main session and every subagent it refuses four things. A force-push. A push to a protected branch. A git command that skips the repository's hooks. Killing processes by name (pkill, killall, taskkill /IM, Stop-Process -Name), because a name match can kill another session's processes.
Three more guards are off until you set them. One bans folders: nothing reads, writes or enters them. One keeps a subagent from editing a plain git checkout, or running a changing git command there, so it works in its own worktree. One stops a subagent reading a big text file whole instead of by offset and limit.
| Option | Default | What it does | | :- | :- | :- | | guard_force_push | on | Refuse --force, --force-with-lease, -f and +refspec pushes | | guard_protected_branches | on | Refuse a push that targets a protected branch | | protected_branches | main, master, dev | The branch names a push cannot target | | guard_no_verify | on | Refuse the git flag that skips hooks | | guard_kill_by_name | on | Refuse killing processes by name | | banned_paths | none | Folders that nothing reads, writes or enters | | guard_shared_checkout | off | Keep subagents out of plain git checkouts | | shared_checkout_root | empty | Limit that guard to checkouts under this folder. Empty detects a plain checkout anywhere: a folder whose .git is a directory, where a linked worktree has a .git file | | read_limit_bytes | 0 (off) | Refuse a subagent's Read of a text file over this size when it gives no limit |
model-allowlistChecks the model a subagent is spawned with.
/plugin install model-allowlist@erikdarling
A pinned model id goes stale: a dated or versioned name keeps pointing at an old model after the tier moves on. This refuses a spawn that names one and asks for a tier alias (opus, sonnet, haiku) instead. A spawn with no model is always allowed, because the agent file or the parent decides then.
You can add rules of your own. One example: the lane agent runs Sonnet, and Opus only when the brief says design, security or hard debugging. With no rules, every alias is allowed.
| Option | Default | What it does | | :- | :- | :- | | refuse_pinned_ids | on | Refuse any model that is not an allowed alias | | allowed_aliases | opus, sonnet, haiku, fable, inherit | The names that count as aliases. A trailing [1m]-style suffix is ignored | | rules | none | One rule per entry, written agent type pattern => model => brief pattern => message |
A rule applies when the spawn's agent type matches the first pattern and it names that model (* for any model). The brief must then match the brief pattern, or the spawn is refused. Leave the brief pattern empty to refuse the pairing outright. The message is optional and can use {type} and {model}. Patterns are case-insensitive regular expressions. A rule that does not parse is skipped. For example:
^(lane|worker-.*)$ => opus => \b(design|security|hard[- ]debug) => {type} runs sonnet; name the reason in the brief to use opus.
seat-resumeBrings interrupted sessions back after a crash, a reboot or a closed terminal.
/plugin install seat-resume@erikdarling
The plugin writes one small file per interactive session: its id, name, folder, permission mode and last activity. It marks the file when the session ends. /resume-sessions lists the sessions that died or were interrupted in the last 72 hours, with the command that reopens each. The script's -Launch switch reopens all of them in terminal tabs. Sessions you left with /exit, Ctrl+C or /clear stay closed. The plugin name still says "seat", but everything you see says "session".
This one is Windows only. The bundled script (scripts/resume-sessions.ps1) is PowerShell. It compares Windows process start times to tell a live session from a reused process id. It reopens tabs in WezTerm or Windows Terminal. Nobody has tried the plugin on macOS or Linux.
| Option | Default | What it does | | :- | :- | :- | | registry_dir | empty: session-registry in your Claude config folder | Where the per-session files go | | sessions_dir | empty: sessions in your Claude config folder | Where Claude Code records its running sessions | | resume_script | empty: the bundled script | The PowerShell script /resume-sessions runs | | powershell | pwsh | The program that runs it (powershell for Windows PowerShell 5.1) | | terminal | wezterm | wezterm or windows-terminal: where -Launch reopens sessions |
The Claude config folder is CLAUDE_CONFIG_DIR when that is set, otherwise .claude in your home folder.
open-asksKeeps the questions Claude asks you from scrolling away. Every time a reply asks you something or leaves a decision to you, Claude records it. The question stays in a band above your prompt, with Claude's recommended answer, until you answer, decline or drop it. Claude gets ask_add, ask_resolve and ask_list tools. You get /asks (done <ids>, clear, hide, show). Open asks are saved per session, so a resumed session still has them.
/plugin install open-asks@erikdarling
| Option | Default | What it does |
|---|---|---|
maxBandAsks | 6 | Most asks the band draws. The rest stay in /asks. |
maxQuestionChars | 400 | Longest question or recommendation shown before it is cut. |
keepDays | 30 | Saved asks from other sessions are removed after this many days. 0 keeps them. |
subagent-bandA live view of your subagents. The band above the prompt has one row per running subagent: type, model, effort, steps, context size, advisor calls and estimated cost. /fleet opens a pane with every subagent of the session, finished ones included. /subagent-cost totals the estimated cost by agent type and by issue number in the description, with the main session's own cost. /steer <id> <text> sends a running subagent a message. Claude gets a cheap subagent_vitals tool, so it does not have to read output files to check progress. Costs are list-price estimates, not your bill.
/plugin install subagent-band@erikdarling
| Option | Default | What it does |
|---|---|---|
priceTable | opus:4:20, sonnet:2:10, haiku:1:5, fable:10:50 | Input and output USD per million tokens for each model family. A model id is matched by containing the family name. |
cacheWriteMultiplier | 1.25 | Cache write price as a multiple of the input price. |
cacheReadMultiplier | 0.1 | Cache read price as a multiple of the input price. |
usage-budgetWatches the account's 5-hour and 7-day usage windows. A toast tells you each time a window crosses a percent. Past a higher percent, Claude also gets a one-time note so it can stop starting optional work and write its handoff. Past the last one, new subagents are refused until the window resets.
/plugin install usage-budget@erikdarling
| Option | Default | What it does |
|---|---|---|
warnPercents | 70, 85, 95 | Percents that raise a toast, once each per window. |
tellModelAtPercent | 85 | From here the model is told as well. 0 never tells it. |
spawnGatePercent | 95 | A new subagent is refused when either window is at or past this. 0 turns the refusal off. |
subagent-wallA wall-clock limit for subagents. After a warning time, a subagent is told to finish up. After the limit, it can only run git and gh commands and write .md or .txt files. It commits, reports and ends instead of running on. It also nudges a code-changing subagent that has a lot of context but no edited file to stop exploring and make the change. The main session is never limited.
/plugin install subagent-wall@erikdarling
| Option | Default | What it does |
|---|---|---|
warnMinutes | 45 | Minutes before a subagent is told to finish up. 0 turns the warning off. |
limitMinutes | 60 | Minutes before it is held to git, gh and note writes. 0 turns the limit off. |
noEditNudgeK | 100 | Thousands of context tokens before the no-edit nudge. 0 turns it off. |
codeAgentTypes | general-purpose | Comma-separated subagent types that get the no-edit nudge. |
This repository is also a pi package: the root package.json declares every plugins/*/skills and plugins/*/extensions directory, and pi reads the same SKILL.md format the other two harnesses do. Install straight from git — no marketplace step:
pi install git:github.com/erikdarlingdata/claude-plugins
As everywhere else, the skill is model-invoked: point pi at a .sqlplan and ask.
pi-session-resume (pi only)Your machine restarts for updates with a dozen pi sessions open; this brings them all back with one command, as terminal tabs, in their original directories, with full history:
pi-resume-sessions
An extension (auto-loaded by the install above) records every open interactive session; the pi-resume-sessions script reopens the interrupted ones — Ghostty tabs on macOS, tmux anywhere. Sessions you quit deliberately (Ctrl+D, /quit) stay closed; sessions killed by a reboot, a closed window, or a crash come back. Idle-time filters keep abandoned sessions from resurrecting.
The script needs a one-time symlink onto your PATH, and macOS needs a one-time Automation permission — see plugins/pi-session-resume/README.md for both, plus the design notes. This one is pi-only: Claude Code and Copilot CLI don't load pi extensions.
pi-subagent-watchdog (pi only)Background subagents only report back when they finish — nothing wakes the orchestrator while one wedges on a giant grep or balloons from 200k to 2M tokens. This extension (auto-loaded by the install above) polls every running subagent's live vitals — tokens, cost, context %, tool uses, turns, wall clock, compactions — and batches nearby threshold crossings into one compact orchestrator check-in. Full structured records persist outside LLM context; per-agent and fleet-wide rate limits keep the watchdog from becoming its own token amplifier. Two orchestrator postures: guide (assess with judgment) and strict (thresholds are budgets — wrap up by default, one evidence-cited extension max). Optional automatic hard stop handles the truly wedged, with the outcome reported from the RPC reply rather than assumed. An optional exact model invariant hard-stops any top-level child that bypasses the manager's pre-spawn model policy. Optional per-agent cost signal and hard stop, plus session and daily USD budget warnings. The CLI surfaces also identify each child's effective model and thinking level.
Humans get a /watchdog panel (vitals, manual check-ins, steering, hard stop) plus /watchdog help | status | config | reload — config edits apply live, no session restart. See plugins/pi-subagent-watchdog/README.md for signals, modes, and design notes. Requires the pi-subagents extension; pi-only for the same reason as above.
pi-subagent-guardrails (pi only)Token-budget guardrails for multi-agent setups, built after an overnight orchestration burned through its usage limit (95% of the spend came at more than 150k context). It has three pieces:
guardrails.md: the written rules for fan-out caps, model tiers, context discipline, session length and ranking.lane agent type for code-editing lanes: Sonnet, its own worktree, draft PRs, no fan-out tools.The wall is deliberately not auto-loaded, because it would wall off your interactive session too. See plugins/pi-subagent-guardrails/README.md for install and the recommended watchdog and pi-subagents settings.
pi-setup-guide.md — a distilled ~15-minute setup for pi written for Claude Code ex-pats: install, model/thinking defaults, the trust model, a Claude-to-pi habit translation table, a tested-together extension stack, full source for a few small quality-of-life extensions (refusal fallback, tab-title status, ! command wake-ups + autocomplete), how to point pi at years of accumulated Claude Code memory instead of migrating it, and a troubleshooting section of the gotchas that actually happened. The plugins in this repo (§11–§13 of the guide) slot into that stack.
Built by Erik Darling at Darling Data. SQL Server consulting, training, and free tools: <https://erikdarling.com>
MIT
hooks/register.tsx 279 lines1import { atom, read, update } from 'claude-code'
2import type { Register, TurnUsage } from 'claude-code'
3
4import type { Row } from '../types'
5
6const rows = atom({ plugin: 'subagent-band', key: 'rows' } as const, {} as Record<string, Row>)
7const isHidden = atom({ plugin: 'subagent-band', key: 'isHidden' } as const, false)
8const mainCost = atom({ plugin: 'subagent-band', key: 'mainCost' } as const, 0)
9
10const blank = (): Row => ({
11 type: '?', description: '', model: '?', effort: '?', steps: 0, context: 0, advisor: 0, cost: 0, isDone: false,
12 startedAt: 0, endedAt: 0, tools: 0, lastTool: '', outcome: '', steered: 0,
13})
14
15const FLEET = 'subagent-fleet'
16const VITALS_TOOL = 'subagent_vitals'
17const shortId = (id: string) => id.replace(/^a/, '').slice(0, 6)
18const mins = (row: Row, now: number) => Math.max(0, Math.round(((row.endedAt || now) - row.startedAt) / 60000))
19
20// One line per subagent: running ones first, then finished ones, newest first.
21const fleetLines = (all: Record<string, Row>, now: number, includeFinished: boolean) => {
22 const entries = Object.entries(all)
23 .filter(([, row]) => includeFinished || !row.isDone)
24 .sort(([, a], [, b]) => Number(a.isDone) - Number(b.isDone) || b.startedAt - a.startedAt)
25
26 return entries.map(([id, row]) =>
27 `${row.isDone ? `done(${row.outcome || '?'})` : 'running'} ${shortId(id)} ${row.type} ${row.model} ${row.effort} ` +
28 `${mins(row, now)}m step ${row.steps} tools ${row.tools}${row.lastTool && !row.isDone ? ` (last ${row.lastTool})` : ''} ` +
29 `ctx ${kilo(row.context)} ${usd(row.cost)}${row.advisor ? ` advisor ${row.advisor}` : ''}` +
30 `${row.steered ? ` steered ${row.steered}` : ''} | ${row.description}`)
31}
32
33// claude-sonnet-5-5-20261001 -> sonnet-5-5
34const shortModel = (id: string) => id.replace(/^claude-/, '').replace(/-\d{8}$/, '')
35const kilo = (n: number) => (n >= 1000 ? `${Math.round(n / 1000)}k` : String(n))
36const pad = (s: string, n: number) => (s.length >= n ? s.slice(0, n) : s + ' '.repeat(n - s.length))
37const usd = (n: number) => `$${n.toFixed(2)}`
38
39// Prices per million tokens (input, output), by model family: "opus:4:20, sonnet:2:10" -> [['opus', 4, 20], ['sonnet', 2, 10]].
40// A model whose id names none of the families is priced as the first one. Estimates, not the bill.
41type Prices = { table: [string, number, number][]; cacheWrite: number; cacheRead: number }
42const DEFAULT_PRICES = 'opus:4:20, sonnet:2:10, haiku:1:5, fable:10:50'
43
44function parsePrices(value: unknown, cacheWrite: unknown, cacheRead: unknown): Prices {
45 const parse = (text: string) =>
46 text
47 .split(',')
48 .map(part => part.trim().split(':'))
49 .flatMap(([family = '', input, output]): [string, number, number][] =>
50 family.trim() && Number.isFinite(Number(input)) && Number.isFinite(Number(output)) && input !== undefined && output !== undefined
51 ? [[family.trim().toLowerCase(), Number(input), Number(output)]]
52 : [])
53 const table = parse(String(value ?? '')).length > 0 ? parse(String(value)) : parse(DEFAULT_PRICES)
54
55 return { table, cacheWrite: Number(cacheWrite ?? 1.25), cacheRead: Number(cacheRead ?? 0.1) }
56}
57
58const stepCost = (u: TurnUsage, prices: Prices) => {
59 const [, input, output] = prices.table.find(([family]) => u.model.toLowerCase().includes(family)) ?? prices.table[0]
60
61 return (u.input_tokens * input + u.cache_creation_input_tokens * prices.cacheWrite * input +
62 u.cache_read_input_tokens * prices.cacheRead * input + u.output_tokens * output) / 1e6
63}
64
65// The /subagent-cost report: this session's subagents by type, by issue number in the description, and the main session.
66const report = (all: Record<string, Row>, main: number) => {
67 const byType = new Map<string, { n: number; cost: number }>()
68 const byIssue = new Map<string, number>()
69 let total = 0
70 for (const row of Object.values(all)) {
71 const t = byType.get(row.type) ?? { n: 0, cost: 0 }
72 byType.set(row.type, { n: t.n + 1, cost: t.cost + row.cost })
73 for (const issue of new Set(row.description.match(/#\d+/g) ?? [])) {
74 byIssue.set(issue, (byIssue.get(issue) ?? 0) + row.cost)
75 }
76 total += row.cost
77 }
78 const lines = [`Estimated list-price cost this session: subagents ${usd(total)} (${Object.keys(all).length}), main session ${usd(main)}`]
79 for (const [type, t] of [...byType].sort((a, b) => b[1].cost - a[1].cost)) {
80 lines.push(` ${pad(type, 18)} ${pad(String(t.n), 4)} ${usd(t.cost)}`)
81 }
82 if (byIssue.size > 0) {
83 lines.push('By issue number in the agent description:')
84 for (const [issue, cost] of [...byIssue].sort((a, b) => b[1] - a[1]).slice(0, 15)) {
85 lines.push(` ${pad(issue, 8)} ${usd(cost)}`)
86 }
87 }
88
89 return lines.join('\n')
90}
91
92export const register: Register = (on, options) => {
93 const prices = parsePrices(options.priceTable ?? DEFAULT_PRICES, options.cacheWriteMultiplier, options.cacheReadMultiplier)
94
95 on('session.start', async ($, e, next) => {
96 await $.command.register({
97 name: 'subagent-band',
98 description: 'Show or hide the band of running subagents (type, model, effort, cost)',
99 })
100 await $.command.register({
101 name: 'subagent-cost',
102 description: "This session's estimated subagent cost by agent type and issue number, and the main session's own",
103 })
104 await $.command.register({ name: 'fleet', description: 'Open a pane with every subagent of this session, running and finished' })
105 await $.command.register({ name: 'steer', description: 'Send a running subagent a message: /steer <id from /fleet> <text>' })
106 await $.tool.register({
107 name: VITALS_TOOL,
108 description:
109 "This session's subagents now: id, type, model, effort, minutes, steps, tool calls, last tool, context, " +
110 'estimated cost, and how finished ones ended. Cheap: use it instead of reading a subagent\'s output file ' +
111 'to check progress. Never call it in a polling loop.',
112 inputSchema: {
113 type: 'object',
114 properties: { includeFinished: { type: 'boolean', description: 'Also list finished subagents (default false).' } },
115 },
116 })
117
118 return next(e)
119 })
120
121 on('command.run', { command: 'fleet' }, async $ => {
122 await $.ui.open({ id: FLEET, title: 'Subagent fleet' })
123
124 return { text: 'Subagent fleet pane opened.' }
125 })
126
127 on('command.run', { command: 'steer' }, async ($, e) => {
128 const [prefix = '', ...words] = e.args.trim().split(/\s+/)
129 const text = words.join(' ').trim()
130 if (!prefix || !text) {
131 return { text: 'Usage: /steer <id from /fleet> <message>' }
132 }
133 const matches = Object.entries(await read($, rows)).filter(([id, row]) => !row.isDone && shortId(id).startsWith(prefix))
134 if (matches.length !== 1) {
135 return { text: matches.length === 0 ? `No running subagent with id ${prefix}.` : `${prefix} matches ${matches.length} subagents; give more of the id.` }
136 }
137 const [id, row] = matches[0]
138 await $.session.send({ to: { agentId: id }, text })
139 await update($, rows, all => ({ ...all, [id]: { ...all[id], steered: all[id].steered + 1 } }))
140
141 return { text: `Sent to ${row.type} ${shortId(id)}${row.steered >= 1 ? ` (follow-up ${row.steered + 1})` : ''}.` }
142 })
143
144 on('tool.call', { tool: 'mcp__subagent-band__subagent_vitals' }, async ($, e) => {
145 const input = e as unknown as { includeFinished?: boolean }
146 const lines = fleetLines(await read($, rows), await $.clock.now(), input.includeFinished === true)
147 const text = lines.length > 0 ? lines.join('\n') : 'No subagents are running.'
148
149 return { result: text } as never
150 })
151
152 // A subagent's tool calls: how many, and the latest.
153 on('tool.call', async ($, e, next) => {
154 const id = e.agentId
155 if (id) {
156 await update($, rows, all => (all[id] ? { ...all, [id]: { ...all[id], tools: all[id].tools + 1, lastTool: e.tool } } : all))
157 }
158
159 return next(e)
160 }).catch(($, e, next) => next(e))
161
162 on('command.run', { command: 'subagent-band' }, async $ => {
163 const hidden = await update($, isHidden, h => !h)
164
165 return { text: hidden ? 'Subagent band hidden.' : 'Subagent band shown.' }
166 })
167
168 on('command.run', { command: 'subagent-cost' }, async $ => ({ text: report(await read($, rows), await read($, mainCost)) }))
169
170 on('agent.spawn', async ($, e, next) => {
171 const spawned = await next(e)
172 const id = spawned.agentId
173 if (id) {
174 const startedAt = await $.clock.now()
175 await update($, rows, all => ({
176 ...all,
177 [id]: { ...blank(), type: e.subagentType, description: e.description, model: shortModel(spawned.model), startedAt },
178 }))
179 }
180
181 return spawned
182 }).catch(($, e, next) => next(e)) // an observer: if this hook fails, the spawn goes ahead (replayed, never run twice)
183
184 // Every model request a subagent makes: the model and effort it actually ran at.
185 on('turn.step', async function* ($, e, next) {
186 const response = yield* next(e)
187 const id = e.agentId
188 if (!id && response.usage) {
189 const cost = stepCost(response.usage, prices)
190 await update($, mainCost, total => total + cost)
191 }
192 if (id) {
193 const usage = response.usage
194 const advisor = (response.serverToolUses ?? []).filter(use => use.name === 'advisor').length
195 await update($, rows, all => {
196 const row = all[id] ?? blank()
197
198 return {
199 ...all,
200 [id]: {
201 ...row,
202 model: shortModel(usage?.model ?? e.model),
203 effort: e.effort === undefined ? 'none' : String(e.effort),
204 steps: row.steps + 1,
205 context: usage
206 ? usage.input_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens
207 : row.context,
208 advisor: row.advisor + advisor,
209 cost: row.cost + (usage ? stepCost(usage, prices) : 0),
210 },
211 }
212 })
213 }
214
215 return response
216 })
217
218 on('turn.complete', async ($, e, next) => {
219 const id = e.agentId
220 if (id) {
221 const endedAt = await $.clock.now()
222 const outcome = e.reason
223 await update($, rows, all => (all[id] ? { ...all, [id]: { ...all[id], isDone: true, endedAt, outcome } } : all))
224 }
225
226 return next(e)
227 })
228
229 on('ui.render', { component: 'Pane', requestId: FLEET }, async ($, e) => {
230 const { Box, Text } = $.ui.resolve(e)
231 const all = await read($, rows)
232 const lines = fleetLines(all, await $.clock.now(), true)
233 const { rateLimits } = await $.session.usage()
234 const usage = rateLimits.map(w => `${w.kind === 'five_hour' ? '5h' : w.kind === 'seven_day' ? '7d' : w.kind} ${Math.round(w.percentUsed)}%`).join(' · ')
235 const room = Math.max(1, (e.viewport?.rows ?? 24) - 4)
236
237 return (
238 <Box flexDirection="column">
239 <Text key="head" bold>
240 {`${Object.values(all).filter(row => !row.isDone).length} running · subagents ${usd(Object.values(all).reduce((sum, row) => sum + row.cost, 0))} · main session ${usd(await read($, mainCost))} (est.)${usage ? ` · usage ${usage}` : ''}`}
241 </Text>
242 {lines.length === 0 && <Text key="none" dimColor>No subagents yet.</Text>}
243 {lines.slice(0, room).map((line, i) => (
244 <Text key={`l${i}`} dimColor={line.startsWith('done')}>{line}</Text>
245 ))}
246 <Text key="help" dimColor>{'/steer <id> <message> sends a running subagent a message.'}</Text>
247 </Box>
248 )
249 })
250
251 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
252 if (e.props.hasSurvey || (await read($, isHidden))) {
253 return next(e)
254 }
255
256 const all = await read($, rows)
257 const live = Object.entries(all).filter(([, row]) => !row.isDone)
258 if (live.length === 0) {
259 return next(e)
260 }
261
262 const { Box, Text } = $.ui.resolve(e)
263
264 return (
265 <Box flexDirection="column">
266 {live.map(([id, row]) => (
267 <Text key={id} dimColor>
268 {pad(row.type, 14)} {pad(row.model, 12)} {pad(row.effort, 6)} step {row.steps} · ctx {kilo(row.context)}
269 {row.advisor ? ` · advisor ${row.advisor}` : ''} · {usd(row.cost)} · {row.description}
270 </Text>
271 ))}
272 <Text key="total" dimColor>
273 {`subagents this session ${usd(Object.values(all).reduce((sum, row) => sum + row.cost, 0))} · main session ${usd(await read($, mainCost))} (est.)`}
274 </Text>
275 </Box>
276 )
277 })
278}
279types/index.d.ts 28 lines1export type Row = {
2 type: string
3 description: string
4 model: string
5 effort: string
6 steps: number
7 context: number
8 advisor: number
9 /** Estimated list-price cost so far, in USD. */
10 cost: number
11 isDone: boolean
12 /** Clock times in ms; endedAt is 0 while it runs. */
13 startedAt: number
14 endedAt: number
15 tools: number
16 lastTool: string
17 /** How its run ended (answer, aborted, error, refusal); '' while it runs. */
18 outcome: string
19 /** How many /steer messages the person sent it. */
20 steered: number
21}
22
23declare module 'claude-code' {
24 interface PluginState {
25 'subagent-band': { rows: Record<string, Row>; isHidden: boolean; mainCost: number }
26 }
27}
28