Shows each running subagent's model and effort on the hint line above the agent list

Claude Code mods by ahbanavi.
Shows the model and effort each running subagent actually uses, at the end of the ← for agents hint line, right above the agent list:
⏵⏵ auto mode on (shift+tab to cycle) · ← for agents · 2× Explore haiku-4-5 · Plan sonnet-5-5/medium · general-purpose opus-5-5/xhigh
● main
○ Explore find the config loader
○ Explore map the test suite
○ Plan plan the migration
○ general-purpose apply the fix
turn.step), so the effort is the one the request really carries, after any downgrade for the model. Nothing is guessed: until a subagent's first request the entry shows its model alone.2× …; finished agents leave the line, killed ones within 5s./plugin marketplace add ahbanavi/claude-mods
/plugin install subagent-models@claude-mods
/reload-plugins
claude plugin validate ./subagent-models
claude plugin test ./subagent-models
claude --plugin-dir ./subagent-modelshooks/register.ts 122 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { SubagentRow } from '../types'
5
6type Running = Record<string, SubagentRow>
7
8const running = atom({ plugin: 'subagent-models', key: 'running' } as const, {} as Running)
9
10const FINISHED = new Set(['completed', 'failed', 'killed'])
11
12// claude-haiku-4-5-20251001 -> haiku-4-5, claude-opus-5-5[1m] -> opus-5-5[1m]
13export const shortModel = (id: string) =>
14 id.replace(/^claude-/, '').replace(/-\d{8}(?=$|\[)/, '')
15
16export const summary = (rows: readonly SubagentRow[]) => {
17 if (rows.length === 0) {
18 return undefined
19 }
20
21 const counts = new Map<string, number>()
22 for (const { label, model, effort } of rows) {
23 const text = `${label} ${model}${effort === undefined ? '' : `/${effort}`}`
24 counts.set(text, (counts.get(text) ?? 0) + 1)
25 }
26
27 const parts = [...counts].map(([text, n]) => (n === 1 ? text : `${n}× ${text}`))
28
29 return ` ${parts.join(' · ')}`
30}
31
32const put = async ($: EngineInterface, id: string, row: SubagentRow) => {
33 await update($, running, all => ({ ...all, [id]: row }))
34}
35
36const drop = async ($: EngineInterface, ids: readonly string[]) => {
37 await update($, running, all =>
38 Object.fromEntries(Object.entries(all).filter(([id]) => !ids.includes(id))),
39 )
40}
41
42// Agents killed without a turn.complete (a /clear, a TaskStop) leave the line here.
43const prune = async ($: EngineInterface) => {
44 const ids = Object.keys(await read($, running))
45 if (ids.length === 0) {
46 return
47 }
48
49 const agents = await $.agent.list()
50 const stale = ids.filter(id => {
51 const agent = agents.find(a => a.id === id)
52 return agent === undefined || FINISHED.has(agent.status)
53 })
54
55 if (stale.length > 0) {
56 await drop($, stale)
57 }
58}
59
60export const register: Register = on => {
61 on('session.start', async ($, e, next) => {
62 const started = await next(e)
63 // Clears the status line an earlier version of this mod pinned.
64 $.ui.status(undefined)
65 $.clock.every(5000, () => void prune($))
66
67 return started
68 })
69
70 // Shows the agent as soon as it starts, before its first request names an effort.
71 on('agent.spawn', async ($, e, next) => {
72 const started = await next(e)
73
74 if (started.deny === undefined && started.agentId !== undefined) {
75 await put($, started.agentId, {
76 label: e.name ?? e.subagentType,
77 model: shortModel(started.model),
78 })
79 }
80
81 return started
82 })
83
84 // Every request a subagent makes names the model and effort it really runs on.
85 on('turn.step', async function* ($, e, next) {
86 if (e.agentId !== undefined) {
87 const known = (await read($, running))[e.agentId]
88 const model = shortModel(e.model)
89 const effort = e.effort === undefined ? undefined : String(e.effort)
90
91 if (known === undefined) {
92 // Compaction and memory forks carry ids $.agent.list() never names.
93 const agent = (await $.agent.list()).find(a => a.id === e.agentId)
94 if (agent !== undefined && !FINISHED.has(agent.status)) {
95 await put($, e.agentId, { label: agent.name ?? agent.type, model, effort })
96 }
97 } else if (known.model !== model || known.effort !== effort) {
98 await put($, e.agentId, { ...known, model, effort })
99 }
100 }
101
102 return yield* next(e)
103 })
104
105 // The dim `← for agents` line sits right above the agent list; the tail rides
106 // its end, so the engine's own line and pills stay live. Writes to `running`
107 // redraw it, since the read here subscribes the line.
108 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
109 const tail = summary(Object.values(await read($, running)))
110
111 return next(tail === undefined ? e : { ...e, props: { ...e.props, tail } })
112 })
113
114 on('turn.complete', async ($, e, next) => {
115 if (e.agentId !== undefined && (await read($, running))[e.agentId] !== undefined) {
116 await drop($, [e.agentId])
117 }
118
119 return next(e)
120 })
121}
122types/index.d.ts 8 lines1export type SubagentRow = { label: string; model: string; effort?: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'subagent-models': { running: Record<string, SubagentRow> }
6 }
7}
8