A live TUI pane showing this session's token usage, cost, context fill and rate limits

A Claude Code mod: a live pane showing the session's token usage — totals, cache hit rate, cost, context window fill, rate limits, a per-request sparkline and a per-model breakdown.
Open it with /tokens.
The repository is private: you need access to it on GitHub, and GitHub set up on your computer.
/plugin install token-dashboard --marketplace mulbrix/claude-mods
Answer y to add the marketplace, then pick a scope.
hooks/register.tsx 212 lines1import { atom, read, update } from 'claude-code'
2import type { Register, TurnUsage } from 'claude-code'
3
4import type { Step, Tokens, Usage } from '../types'
5
6const PANE = 'token-dashboard'
7const MAX_STEPS = 500
8const SPARK = '▁▂▃▄▅▆▇█'
9
10const zero = (): Tokens => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 })
11const empty = (): Usage => ({
12 total: zero(),
13 requests: 0,
14 byModel: {},
15 main: zero(),
16 agents: zero(),
17 steps: [],
18})
19
20const usage = atom({ plugin: 'token-dashboard', key: 'usage' } as const, empty())
21
22const add = <T extends Tokens>(a: T, b: Tokens): T => ({
23 ...a,
24 input: a.input + b.input,
25 output: a.output + b.output,
26 cacheRead: a.cacheRead + b.cacheRead,
27 cacheWrite: a.cacheWrite + b.cacheWrite,
28})
29
30const fromApi = (u: TurnUsage): Tokens => ({
31 input: u.input_tokens,
32 output: u.output_tokens,
33 cacheRead: u.cache_read_input_tokens,
34 cacheWrite: u.cache_creation_input_tokens,
35})
36
37const sum = (t: Tokens) => t.input + t.output + t.cacheRead + t.cacheWrite
38
39export const record = (prev: Usage, step: Step): Usage => {
40 const model = prev.byModel[step.model] ?? { ...zero(), requests: 0 }
41 return {
42 total: add(prev.total, step),
43 requests: prev.requests + 1,
44 byModel: { ...prev.byModel, [step.model]: { ...add(model, step), requests: model.requests + 1 } },
45 main: step.isAgent ? prev.main : add(prev.main, step),
46 agents: step.isAgent ? add(prev.agents, step) : prev.agents,
47 steps: [...prev.steps, step].slice(-MAX_STEPS),
48 }
49}
50
51export const fmt = (n: number): string => {
52 if (n < 1000) return String(n)
53 if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`
54 return `${(n / 1e6).toFixed(n < 1e7 ? 2 : 1)}M`
55}
56
57export const bar = (pct: number, width: number): string => {
58 const w = Math.max(4, width)
59 const filled = Math.round((Math.min(100, Math.max(0, pct)) / 100) * w)
60 return '█'.repeat(filled) + '░'.repeat(w - filled)
61}
62
63export const sparkline = (values: number[]): string => {
64 const max = Math.max(1, ...values)
65 return values.map(v => SPARK[Math.min(SPARK.length - 1, Math.floor((v / max) * (SPARK.length - 1)))]).join('')
66}
67
68const pctColor = (pct: number) => (pct >= 85 ? 'error' : pct >= 60 ? 'warning' : 'success')
69
70const shortModel = (id: string) => id.replace(/^claude-/, '').replace(/-\d{8}$/, '')
71
72const LIMIT_LABELS: Record<string, string> = { five_hour: '5-hour', seven_day: '7-day', spend_limit: 'Spend' }
73
74const resetsIn = (iso: string | undefined, now: number) => {
75 if (!iso) return ''
76 const mins = Math.max(0, Math.round((Date.parse(iso) - now) / 60000))
77 return mins >= 60 * 24
78 ? `resets in ${Math.floor(mins / 1440)}d ${Math.floor((mins % 1440) / 60)}h`
79 : `resets in ${Math.floor(mins / 60)}h ${mins % 60}m`
80}
81
82export const register: Register = on => {
83 on('session.start', async ($, e, next) => {
84 await $.command.register({
85 name: 'tokens',
86 description: 'Open the token usage dashboard',
87 })
88
89 return next(e)
90 })
91
92 on('command.run', { command: 'tokens' }, async $ => {
93 await $.ui.open({ id: PANE, title: 'Tokens', focus: true })
94
95 return { text: 'Token dashboard opened.' }
96 })
97
98 on('turn.step', async function* ($, e, next) {
99 const result = yield* next(e)
100 if (result.usage) {
101 const u = result.usage
102 const step: Step = { ...fromApi(u), at: await $.clock.now(), model: u.model, isAgent: e.agentId !== undefined }
103 await update($, usage, prev => record(prev, step))
104 }
105
106 return result
107 })
108
109 on('session.measure', async ($, e, next) => {
110 $.ui.invalidate('ui.render')
111
112 return next(e)
113 })
114
115 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
116 const { Box, Text, Button } = $.ui.resolve(e)
117 const u = await read($, usage)
118 const session = await $.session.usage()
119 const now = await $.clock.now()
120 const cols = Math.max(30, e.props.bodyColumns)
121 const barWidth = Math.min(40, cols - 22)
122
123 const ctx = session.context
124 const inputSide = u.total.input + u.total.cacheRead + u.total.cacheWrite
125 const hitRate = inputSide === 0 ? 0 : Math.round((u.total.cacheRead / inputSide) * 100)
126 const spark = sparkline(u.steps.slice(-Math.max(8, cols - 4)).map(sum))
127 const models = Object.entries(u.byModel).sort((a, b) => sum(b[1]) - sum(a[1]))
128
129 const row = (label: string, value: string, color?: string) => (
130 <Box>
131 <Box width={12}><Text dimColor>{label}</Text></Box>
132 <Text color={color} bold>{value}</Text>
133 </Box>
134 )
135
136 return (
137 <Box flexDirection="column" paddingX={1}>
138 <Box justifyContent="space-between">
139 <Text bold color="claude">Token usage</Text>
140 <Text bold color="success">{session.cost ? `$${session.cost.usd.toFixed(2)}` : ''}</Text>
141 </Box>
142
143 <Box flexDirection="column" marginTop={1}>
144 {row('Total', `${fmt(sum(u.total))} over ${u.requests} request${u.requests === 1 ? '' : 's'}`)}
145 {row('Input', fmt(u.total.input))}
146 {row('Output', fmt(u.total.output))}
147 {row('Cache read', fmt(u.total.cacheRead))}
148 {row('Cache write', fmt(u.total.cacheWrite))}
149 {row('Cache hit', `${hitRate}%`, hitRate >= 70 ? 'success' : hitRate >= 40 ? 'warning' : 'error')}
150 {u.agents.output + u.agents.input > 0 &&
151 row('Subagents', `${fmt(sum(u.agents))} (main ${fmt(sum(u.main))})`)}
152 </Box>
153
154 <Box flexDirection="column" marginTop={1}>
155 <Text bold>Limits</Text>
156 <Box>
157 <Box width={12}><Text dimColor>Context</Text></Box>
158 {ctx.percent === undefined ? (
159 <Text dimColor>no response yet · {fmt(ctx.window)} window</Text>
160 ) : (
161 <Text>
162 <Text color={pctColor(ctx.percent)}>{bar(ctx.percent, barWidth)}</Text> {ctx.percent}%{' '}
163 <Text dimColor>{fmt(ctx.tokens ?? 0)}/{fmt(ctx.window)}</Text>
164 </Text>
165 )}
166 </Box>
167 {session.rateLimits.map(rl => (
168 <Box>
169 <Box width={12}><Text dimColor>{LIMIT_LABELS[rl.kind] ?? rl.kind}</Text></Box>
170 <Text>
171 <Text color={pctColor(rl.percentUsed)}>{bar(rl.percentUsed, barWidth)}</Text> {rl.percentUsed}%{' '}
172 <Text dimColor>{resetsIn(rl.resetsAt, now)}</Text>
173 </Text>
174 </Box>
175 ))}
176 </Box>
177
178 <Box flexDirection="column" marginTop={1}>
179 <Text bold>Per request <Text dimColor>(last {Math.min(u.steps.length, cols - 4)})</Text></Text>
180 {u.steps.length === 0 ? <Text dimColor>No requests yet.</Text> : <Text color="claude">{spark}</Text>}
181 {u.steps.length > 0 && (
182 <Text dimColor>
183 last {fmt(sum(u.steps[u.steps.length - 1] ?? zero()))} · peak {fmt(Math.max(...u.steps.map(sum)))} · avg{' '}
184 {fmt(Math.round(sum(u.total) / Math.max(1, u.requests)))}
185 </Text>
186 )}
187 </Box>
188
189 {models.length > 0 && (
190 <Box flexDirection="column" marginTop={1}>
191 <Text bold>By model</Text>
192 {models.map(([id, m]) => (
193 <Box>
194 <Box width={Math.min(24, cols - 30)}><Text>{shortModel(id)}</Text></Box>
195 <Text dimColor>
196 in {fmt(m.input + m.cacheRead + m.cacheWrite).padStart(6)} out {fmt(m.output).padStart(6)} ×{m.requests}
197 </Text>
198 </Box>
199 ))}
200 </Box>
201 )}
202
203 <Box marginTop={1}>
204 <Button hotkey="r" plain dimColor onPress={async () => { await update($, usage, () => empty()) }}>
205 Reset counters
206 </Button>
207 </Box>
208 </Box>
209 )
210 })
211}
212types/index.d.ts 19 lines1export type Tokens = { input: number; output: number; cacheRead: number; cacheWrite: number }
2
3export type Step = Tokens & { at: number; model: string; isAgent: boolean }
4
5export type Usage = {
6 total: Tokens
7 requests: number
8 byModel: Record<string, Tokens & { requests: number }>
9 main: Tokens
10 agents: Tokens
11 steps: Step[]
12}
13
14declare module 'claude-code' {
15 interface PluginState {
16 'token-dashboard': { usage: Usage }
17 }
18}
19