Live context, rate limit and cost meter above the prompt

A live band above the prompt showing how much of your context, rate limits and money the session is using, and what the last turn added.
ctx ▓▓▓▓▓▓░░░░ 58% (116k/200k) · 5h: 23% (resets 2h14m) · 7d: 65% (resets 1d17h) · $1.84
last turn +12k ctx, +$0.21
| Item | Meaning |
|---|---|
ctx | Context window fill: tokens the last response was answered over, against the model's window |
5h / 7d | Subscription rate limit windows and the time until each resets (Pro/Max only; absent with an API key) |
$ | Session cost so far, as /cost totals it |
last turn | Context growth and cost of the last main turn, subagent spend included. Negative after a /compact |
A toast warns each time the context crosses 80%.
Reads the session's usage figures only. No network, no files, no model calls, no cost.
/plugin install usage-meter --marketplace TomDiem/claude-code-modshooks/register.tsx 138 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Turn, Usage } from '../types'
5
6const usage = atom({ plugin: 'usage-meter', key: 'usage' } as const, null)
7const last = atom({ plugin: 'usage-meter', key: 'last' } as const, null)
8const now = atom({ plugin: 'usage-meter', key: 'now' } as const, 0)
9
10const MINUTE = 60_000
11const WARN_PERCENT = 80
12const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d' }
13
14const BAR_WIDTH = 10
15
16const k = (tokens: number) => `${Math.round(tokens / 1000)}k`
17
18const bar = (percent: number) => {
19 const filled = Math.round((Math.min(percent, 100) / 100) * BAR_WIDTH)
20 return '▓'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled)
21}
22
23const dollars = (amount: number) => `$${amount.toFixed(2)}`
24
25const signed = (n: number, unit: (n: number) => string) => `${n < 0 ? '-' : '+'}${unit(Math.abs(n))}`
26
27const duration = (ms: number) => {
28 const minutes = Math.max(0, Math.round(ms / MINUTE))
29 const days = Math.floor(minutes / 1440)
30 const hours = Math.floor((minutes % 1440) / 60)
31
32 if (days > 0) {
33 return `${days}d${hours}h`
34 }
35
36 return hours > 0 ? `${hours}h${minutes % 60}m` : `${minutes}m`
37}
38
39const totals = ({ context, rateLimits, usd }: Usage, time: number) => {
40 const parts: string[] = []
41
42 if (context.tokens !== undefined && context.percent !== undefined) {
43 parts.push(`ctx ${bar(context.percent)} ${context.percent}% (${k(context.tokens)}/${k(context.window)})`)
44 }
45
46 for (const { kind, percentUsed, resetsAt } of rateLimits) {
47 const reset = resetsAt ? ` (resets ${duration(Date.parse(resetsAt) - time)})` : ''
48 parts.push(`${LABELS[kind] ?? kind}: ${percentUsed}%${reset}`)
49 }
50
51 if (usd !== undefined) {
52 parts.push(dollars(usd))
53 }
54
55 return parts.join(' · ')
56}
57
58const delta = ({ tokens, usd }: Turn) => `last turn ${signed(tokens, k)} ctx, ${signed(usd, dollars)}`
59
60const snapshot = async ($: EngineInterface): Promise<Turn | null> => {
61 const { context, cost } = await $.session.usage()
62 return context.tokens === undefined ? null : { tokens: context.tokens, usd: cost?.usd ?? 0 }
63}
64
65export const register: Register = on => {
66 let baseline: Turn | null = null
67
68 on('session.start', async ($, e, next) => {
69 const tick = async () => {
70 const time = await $.clock.now()
71 await update($, now, () => time)
72 }
73
74 await tick()
75 $.clock.every(MINUTE, tick)
76
77 return next(e)
78 })
79
80 on('session.measure', async ($, e, next) => {
81 const previous = (await read($, usage))?.context.percent ?? 0
82 const percent = e.context.percent ?? 0
83
84 if (previous < WARN_PERCENT && percent >= WARN_PERCENT) {
85 $.ui.toast(`Context at ${percent}%: consider /compact`)
86 }
87
88 await update($, usage, () => ({ context: e.context, rateLimits: e.rateLimits, usd: e.cost?.usd }))
89
90 return next(e)
91 })
92
93 on('turn.start', async ($, e, next) => {
94 if (!e.agentId) {
95 baseline = await snapshot($)
96 }
97
98 return next(e)
99 })
100
101 on('turn.complete', async ($, e, next) => {
102 const before = baseline
103
104 if (!e.agentId && before) {
105 baseline = null
106 const after = await snapshot($)
107
108 if (after) {
109 await update($, last, () => ({ tokens: after.tokens - before.tokens, usd: after.usd - before.usd }))
110 }
111 }
112
113 return next(e)
114 })
115
116 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
117 const rest = await next(e)
118 const current = await read($, usage)
119 const turn = await read($, last)
120 const summary = current ? totals(current, await read($, now)) : ''
121 const change = turn ? delta(turn) : ''
122
123 if (e.props.hasSurvey || (!summary && !change)) {
124 return rest
125 }
126
127 const { Box, Text } = $.ui.resolve(e)
128
129 return (
130 <Box flexDirection="column">
131 {rest}
132 {summary ? <Text dimColor>{summary}</Text> : null}
133 {change ? <Text dimColor>{change}</Text> : null}
134 </Box>
135 )
136 })
137}
138types/index.d.ts 14 lines1export type Usage = {
2 context: { tokens?: number; window: number; percent?: number }
3 rateLimits: { kind: string; percentUsed: number; resetsAt?: string }[]
4 usd?: number
5}
6
7export type Turn = { tokens: number; usd: number }
8
9declare module 'claude-code' {
10 interface PluginState {
11 'usage-meter': { usage: Usage | null; last: Turn | null; now: number }
12 }
13}
14