Band above the prompt: after a long run, which model and effort the main session, each subagent, Gemini and Ollama used

After a long run, a band above the prompt lists which model and effort level each part of the work ran under.
When a prompt takes longer than the threshold, the band shows:
Last run took 2m 10s
- Main session: opus-5-5, effort high
- Explore: find auth code: sonnet-5-5, effort medium
- Gemini: gemini-analyze-code x2
- Ollama: qwen2.5:3b (explain_file)
mcp__gemini__* and mcp__ollama-claude__* tools, with a count when called more than once. These are tool calls to MCP servers, not subagents, so they're detected by tool name. The Ollama model name appears only if the call carries a model argument; otherwise it says default model.The band clears when you submit your next prompt.
Mixed setups (different models for the main session, subagents and delegated tools) make it easy to lose track of what ran where.
claude --plugin-dir /path/to/claude-code-mods/run-breakdown
To load it in every session, see Install.
In hooks/register.tsx:
MIN_MS is how long a prompt must take before the band appears. It is 5 seconds while testing; 30 seconds is the planned everyday value.tool.call hook that matches their tool-name prefix. The two MCP prefixes are specific to the author's setup, so change them to match yours.No tokens. It only reads what the engine already knows about each request.
turn.step records each request's model and effort per loop (main or subagent id), tool.call counts Gemini and Ollama calls, turn.complete builds the list when the run was long enough, and a ui.render hook on the AbovePrompt band draws it.
hooks/register.tsx 105 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Breakdown } from '../types'
5
6// Only runs at least this long get a breakdown (5 s while testing; 30 s planned).
7const MIN_MS = 5_000
8
9const breakdown = atom({ plugin: 'run-breakdown', key: 'breakdown' } as const, null as Breakdown | null)
10
11// What this prompt's run used; reset on each prompt. Key 'main' or a subagent id.
12const loops = new Map<string, { model: string; effort: string }>()
13const external = new Map<string, number>()
14
15const short = (model: string) => model.replace(/^claude-/, '').replace(/-\d{8}$/, '')
16
17function duration(ms: number) {
18 const s = Math.round(ms / 1000)
19
20 return s >= 60 ? `${Math.floor(s / 60)}m ${s % 60}s` : `${s}s`
21}
22
23async function build($: EngineInterface, ms: number): Promise<Breakdown> {
24 const agents = await $.agent.list()
25 const lines: string[] = []
26
27 for (const [key, run] of loops) {
28 const who =
29 key === 'main'
30 ? 'Main session'
31 : (a => (a ? `${a.type}: ${a.description}` : 'Subagent'))(agents.find(x => x.id === key))
32 lines.push(`${who}: ${short(run.model)}, effort ${run.effort}`)
33 }
34
35 for (const [name, count] of external) {
36 lines.push(`${name}${count > 1 ? ` x${count}` : ''}`)
37 }
38
39 return { took: duration(ms), lines }
40}
41
42export const register: Register = on => {
43 on('prompt.submit', async ($, e, next) => {
44 loops.clear()
45 external.clear()
46 await update($, breakdown, () => null)
47
48 return next(e)
49 })
50
51 // Each request names its model and effort, in the main loop or a subagent's.
52 on('turn.step', async function* ($, e, next) {
53 loops.set(e.agentId ?? 'main', { model: e.model, effort: String(e.effort ?? 'default') })
54
55 return yield* next(e)
56 })
57
58 // Gemini and Ollama are MCP tool calls, not agents.
59 on('tool.call', ($, e, next) => {
60 const tool = String(e.tool)
61 const args = e as unknown as Record<string, unknown>
62 const name = tool.startsWith('mcp__gemini__')
63 ? `Gemini: ${tool.slice('mcp__gemini__'.length)}`
64 : tool.startsWith('mcp__ollama-claude__')
65 ? `Ollama: ${typeof args.model === 'string' ? args.model : 'default model'} (${tool.slice('mcp__ollama-claude__ollama_'.length)})`
66 : null
67
68 if (name) {
69 external.set(name, (external.get(name) ?? 0) + 1)
70 }
71
72 return next(e)
73 })
74
75 on('turn.complete', async ($, e, next) => {
76 const done = await next(e)
77
78 if (!e.agentId && e.durationMs >= MIN_MS) {
79 const result = await build($, e.durationMs)
80 await update($, breakdown, () => result)
81 }
82
83 return done
84 })
85
86 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
87 const shown = await read($, breakdown)
88
89 if (e.props.hasSurvey || !shown) {
90 return next(e)
91 }
92
93 const { Box, Text } = $.ui.resolve(e)
94
95 return (
96 <Box flexDirection="column">
97 <Text bold>Last run took {shown.took}</Text>
98 {shown.lines.map((line, i) => (
99 <Text key={`l${i}`} dimColor>- {line}</Text>
100 ))}
101 </Box>
102 )
103 })
104}
105types/index.d.ts 8 lines1export type Breakdown = { took: string; lines: string[] }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'run-breakdown': { breakdown: Breakdown | null }
6 }
7}
8