SLOPSHOPPER

run-breakdown

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

newbandguardprompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · run-breakdown
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ 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 Last run took 42s ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Last run took 42s
README

run-breakdown

After a long run, a band above the prompt lists which model and effort level each part of the work ran under.

What it does

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)
  • Main session: the model and effort of the main conversation. This also shows when there are no subagents.
  • Subagents: one line per subagent that ran, with its model and effort.
  • Gemini and Ollama: calls to the 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.

Why

Mixed setups (different models for the main session, subagents and delegated tools) make it easy to lose track of what ran where.

Install and run

claude --plugin-dir /path/to/claude-code-mods/run-breakdown

To load it in every session, see Install.

Settings

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.
  • To track other external tools, add a branch in the 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.

Cost

No tokens. It only reads what the engine already knows about each request.

How it works

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.

Source 2 files
hooks/register.tsx 105 lines
1import { 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}
105
types/index.d.ts 8 lines
1export type Breakdown = { took: string; lines: string[] }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'run-breakdown': { breakdown: Breakdown | null }
6  }
7}
8