SLOPSHOPPER

subagent-models

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

newspinnerstatustimeragents
★ 1v0.1.0MITupdated 2026-10-02ahbanavi/claude-mods/subagent-models
A shopper browsing a rack in a slop shop
README

claude-mods

Claude Code mods by ahbanavi.

subagent-models

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
  • Values come from each request the subagent sends (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.
  • Models that take no effort setting show no effort.
  • Identical agents collapse into 2× …; finished agents leave the line, killed ones within 5s.
  • Terminal only for now: other surfaces don't draw the hint line's tail yet.

Install

/plugin marketplace add ahbanavi/claude-mods
/plugin install subagent-models@claude-mods
/reload-plugins

Develop

claude plugin validate ./subagent-models
claude plugin test ./subagent-models
claude --plugin-dir ./subagent-models
Source 2 files
hooks/register.ts 122 lines
1import { 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}
122
types/index.d.ts 8 lines
1export 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