Band above the prompt: session tokens, cost, and what remains of the 5-hour and weekly limits.

hooks/register.tsx 119 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Meter } from '../types'
5
6const empty: Meter = {
7 tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
8 usd: null,
9 limits: [],
10}
11const meter = atom({ plugin: 'usage-meter', key: 'meter' } as const, empty)
12
13const fmt = (n: number) =>
14 n >= 1e6 ? `${(n / 1e6).toFixed(2)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : `${n}`
15
16const until = (iso: string | undefined, now: number) => {
17 if (!iso) return ''
18 const mins = Math.max(0, Math.round((Date.parse(iso) - now) / 60000))
19 const d = Math.floor(mins / 1440)
20 const h = Math.floor((mins % 1440) / 60)
21 const m = mins % 60
22 return d > 0 ? ` resets ${d}d${h}h` : h > 0 ? ` resets ${h}h${m}m` : ` resets ${m}m`
23}
24
25const PANE = 'usage-meter'
26const name = (k: string) => (k === 'five_hour' ? '5h' : k === 'seven_day' ? 'week' : k)
27
28const line = (m: Meter, now: number) => {
29 const t = m.tokens
30 const total = t.input + t.output + t.cacheRead + t.cacheWrite
31 return (
32 `tokens ${fmt(total)} (in ${fmt(t.input)} · out ${fmt(t.output)} · cache ${fmt(t.cacheRead + t.cacheWrite)})` +
33 (m.usd !== null ? ` · $${m.usd.toFixed(2)}` : '') +
34 m.limits
35 .map(l => ` · ${name(l.kind)} ${Math.max(0, 100 - l.percentUsed).toFixed(0)}% left${until(l.resetsAt, now)}`)
36 .join('')
37 )
38}
39
40export const register: Register = on => {
41 on('session.start', async ($, e, next) => {
42 const u = await $.session.usage()
43 const m = await update($, meter, m => ({ ...m, usd: u.cost?.usd ?? null, limits: u.rateLimits }))
44 $.ui.status(line(m, await $.clock.now()))
45 await $.command.register({ name: 'usage-meter', description: 'Show token usage, cost and limits in a pane' })
46 return next(e)
47 })
48
49 on('turn.complete', async ($, e, next) => {
50 const u = e.usage
51 if (u) {
52 const m = await update($, meter, m => ({
53 ...m,
54 tokens: {
55 input: m.tokens.input + u.input_tokens,
56 output: m.tokens.output + u.output_tokens,
57 cacheRead: m.tokens.cacheRead + u.cache_read_input_tokens,
58 cacheWrite: m.tokens.cacheWrite + u.cache_creation_input_tokens,
59 },
60 }))
61 $.ui.status(line(m, await $.clock.now()))
62 }
63 return next(e)
64 })
65
66 on('session.measure', async ($, e, next) => {
67 const m = await update($, meter, m => ({ ...m, usd: e.cost?.usd ?? m.usd, limits: e.rateLimits }))
68 $.ui.status(line(m, await $.clock.now()))
69 return next(e)
70 })
71
72 on('command.run', { command: 'usage-meter' }, async $ => {
73 void $.ui.open({ id: PANE, title: 'Usage' })
74 const m = await read($, meter)
75 const now = await $.clock.now()
76 const t = m.tokens
77 const rows = [
78 `Tokens in ${fmt(t.input)} · out ${fmt(t.output)} · cache read ${fmt(t.cacheRead)} · cache write ${fmt(t.cacheWrite)}`,
79 `Cost ${m.usd !== null ? `$${m.usd.toFixed(2)}` : 'n/a'}`,
80 ...(m.limits.length === 0
81 ? ['Limits no 5h / week data reported yet (needs a subscription plan and one finished turn)']
82 : m.limits.map(l => `${name(l.kind).padEnd(8)} ${Math.max(0, 100 - l.percentUsed).toFixed(0)}% left (${l.percentUsed}% used)${until(l.resetsAt, now)}`)),
83 ]
84 return { text: rows.join('\n') }
85 })
86
87 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
88 const m = await read($, meter)
89 const now = await $.clock.now()
90 const { Box, Text } = $.ui.resolve(e)
91 const t = m.tokens
92 return (
93 <Box flexDirection="column">
94 <Text>Tokens in: {fmt(t.input)}</Text>
95 <Text>Tokens out: {fmt(t.output)}</Text>
96 <Text>Cache read / write: {fmt(t.cacheRead)} / {fmt(t.cacheWrite)}</Text>
97 <Text>Cost: {m.usd !== null ? `$${m.usd.toFixed(2)}` : 'n/a'}</Text>
98 {m.limits.length === 0 && <Text dimColor>No 5h / week limit data (needs a subscription plan).</Text>}
99 {m.limits.map(l => (
100 <Text>
101 {name(l.kind)}: {Math.max(0, 100 - l.percentUsed).toFixed(0)}% left{until(l.resetsAt, now)}
102 </Text>
103 ))}
104 </Box>
105 )
106 })
107
108 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
109 if (e.props.hasSurvey) return next(e)
110 const m = await read($, meter)
111 const { Box, Text } = $.ui.resolve(e)
112 return (
113 <Box>
114 <Text dimColor>{line(m, await $.clock.now())}</Text>
115 </Box>
116 )
117 })
118}
119types/index.d.ts 10 lines1export type Tokens = { input: number; output: number; cacheRead: number; cacheWrite: number }
2export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
3export type Meter = { tokens: Tokens; usd: number | null; limits: Limit[] }
4
5declare module 'claude-code' {
6 interface PluginState {
7 'usage-meter': { meter: Meter }
8 }
9}
10