Shows the agent's busy time, turn count, tool calls, tokens, context fill and cost above the prompt.

A Claude Code mod that draws a small stats band above the prompt:
Busy 02:06 · 9 turns · 15 tool calls Context 13% · 15.0k in (100% cached) 72 out
Bash 4 · Read 3 $0.99
The busy time, turns and tool calls also appear in the status line while the agent works.
Install it from the marketplace in this repository; see the repository README.
hooks/register.tsx 187 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Stats } from '../types'
5
6const EMPTY: Stats = {
7 total: 0,
8 since: null,
9 now: 0,
10 turns: 0,
11 tools: 0,
12 byTool: {},
13 subMs: 0,
14 subTurns: 0,
15 inputTokens: 0,
16 outputTokens: 0,
17 cacheReadTokens: 0,
18 cacheWriteTokens: 0,
19 context: null,
20 usd: null,
21}
22
23const stats = atom({ plugin: 'session-status', key: 'stats' } as const, EMPTY)
24
25// State kept across a reload may predate a field, so fill gaps from EMPTY.
26const patch = (s: Stats, change: Partial<Stats>): Stats => ({ ...EMPTY, ...s, ...change })
27
28const SHOWN = 3
29
30const format = (ms: number) => {
31 const seconds = Math.floor(ms / 1000)
32 const h = Math.floor(seconds / 3600)
33 const m = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0')
34 const s = String(seconds % 60).padStart(2, '0')
35
36 return h > 0 ? `${h}:${m}:${s}` : `${m}:${s}`
37}
38
39const compact = (n: number) =>
40 n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n)
41
42const label = (tool: string) => (tool.startsWith('mcp__') ? tool.split('__').pop() ?? tool : tool)
43
44const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? '' : 's'}`
45
46const busy = (s: Stats) => s.total + (s.since === null ? 0 : s.now - s.since)
47
48const join = (parts: (string | null)[]) => parts.filter(Boolean).join(' · ')
49
50const summary = (s: Stats) =>
51 join([
52 `Busy ${format(busy(s))}`,
53 plural(s.turns, 'turn'),
54 plural(s.tools, 'tool call'),
55 s.subTurns > 0 ? `Subagents ${format(s.subMs)} (${s.subTurns})` : null,
56 ])
57
58const byTool = (s: Stats) => {
59 const ranked = Object.entries(s.byTool).sort((a, b) => b[1] - a[1])
60 const shown = ranked.slice(0, SHOWN).map(([tool, n]) => `${label(tool)} ${n}`)
61 const rest = ranked.slice(SHOWN).reduce((sum, [, n]) => sum + n, 0)
62
63 return rest > 0 ? [...shown, `other ${rest}`] : shown
64}
65
66const tokens = (s: Stats) => {
67 const input = s.inputTokens + s.cacheReadTokens + s.cacheWriteTokens
68
69 if (input + s.outputTokens === 0) {
70 return null
71 }
72
73 const cached = input === 0 ? 0 : Math.round((s.cacheReadTokens / input) * 100)
74
75 return `${compact(input)} in (${cached}% cached) ${compact(s.outputTokens)} out`
76}
77
78// Row one: time and counts on the left, context and tokens on the right. Row two: tools on the left, cost on the right.
79const rows = (s: Stats) => ({
80 left: summary(s),
81 right: join([s.context === null ? null : `Context ${s.context}%`, tokens(s)]),
82 tools: byTool(s).join(' · '),
83 usd: s.usd === null ? '' : `$${s.usd.toFixed(2)}`,
84})
85
86export const register: Register = on => {
87 on('session.start', async ($, e, next) => {
88 $.clock.every(1000, async () => {
89 const current = patch(await read($, stats), {})
90
91 if (current.since === null) {
92 return
93 }
94
95 const now = await $.clock.now()
96 await update($, stats, s => patch(s, { now }))
97 $.ui.status(summary({ ...current, now }))
98 })
99
100 return next(e)
101 })
102
103 on('turn.start', async ($, e, next) => {
104 if (e.agentId === undefined) {
105 const now = await $.clock.now()
106 await update($, stats, s => patch(s, { since: s.since ?? now, now, turns: s.turns + 1 }))
107 }
108
109 return next(e)
110 })
111
112 on('tool.call', async ($, e, next) => {
113 await update($, stats, s => {
114 const counts = patch(s, {}).byTool
115
116 return patch(s, { tools: s.tools + 1, byTool: { ...counts, [e.tool]: (counts[e.tool] ?? 0) + 1 } })
117 })
118
119 return next(e)
120 })
121
122 on('turn.complete', async ($, e, next) => {
123 const now = await $.clock.now()
124 const usage = e.usage
125
126 if (usage !== undefined) {
127 await update($, stats, s => {
128 const current = patch(s, {})
129
130 return patch(s, {
131 inputTokens: current.inputTokens + usage.input_tokens,
132 outputTokens: current.outputTokens + usage.output_tokens,
133 cacheReadTokens: current.cacheReadTokens + usage.cache_read_input_tokens,
134 cacheWriteTokens: current.cacheWriteTokens + usage.cache_creation_input_tokens,
135 })
136 })
137 }
138
139 if (e.agentId === undefined) {
140 await update($, stats, s =>
141 patch(s, {
142 total: s.since === null ? s.total : s.total + now - s.since,
143 since: null,
144 now,
145 }),
146 )
147 $.ui.status(undefined)
148 } else {
149 await update($, stats, s =>
150 patch(s, { subMs: (s.subMs ?? 0) + e.durationMs, subTurns: (s.subTurns ?? 0) + 1 }),
151 )
152 }
153
154 return next(e)
155 })
156
157 on('session.measure', async ($, e, next) => {
158 await update($, stats, s =>
159 patch(s, { context: e.context.percent ?? s.context, usd: e.cost?.usd ?? s.usd }),
160 )
161
162 return next(e)
163 })
164
165 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
166 if (e.props.hasSurvey) {
167 return next(e)
168 }
169
170 const { Box, Text } = $.ui.resolve(e)
171 const { left, right, tools, usd } = rows(patch(await read($, stats), {}))
172
173 return (
174 <Box flexDirection="column">
175 <Box justifyContent="space-between">
176 <Text dimColor>{left}</Text>
177 <Text dimColor>{right}</Text>
178 </Box>
179 <Box justifyContent="space-between">
180 <Text dimColor>{tools}</Text>
181 <Text dimColor>{usd}</Text>
182 </Box>
183 </Box>
184 )
185 })
186}
187types/index.d.ts 37 lines1export type Stats = {
2 /** Milliseconds of finished busy turns. */
3 total: number
4 /** When the running main turn started, or null while idle. */
5 since: number | null
6 /** The latest tick while a turn runs. */
7 now: number
8 /** Main-agent turns started. */
9 turns: number
10 /** Tool calls made, subagents' included. */
11 tools: number
12 /** Tool calls by tool name. */
13 byTool: Record<string, number>
14 /** Milliseconds of finished subagent turns, summed; parallel runs overlap. */
15 subMs: number
16 /** Subagent turns finished. */
17 subTurns: number
18 /** Uncached input tokens, every turn and subagent turn summed. */
19 inputTokens: number
20 /** Output tokens, summed. */
21 outputTokens: number
22 /** Input tokens the prompt cache served, summed. */
23 cacheReadTokens: number
24 /** Input tokens written to the prompt cache, summed. */
25 cacheWriteTokens: number
26 /** Context window fill as a whole percentage, or null before the first response. */
27 context: number | null
28 /** Cost so far in US dollars, or null where the host keeps no ledger. */
29 usd: number | null
30}
31
32declare module 'claude-code' {
33 interface PluginState {
34 'session-status': { stats: Stats }
35 }
36}
37