Band above the prompt: 5h/7d rate-limit usage with reset countdowns, session cache tokens and cost

A band above the Claude Code prompt showing how much of your plan you've used and what the session costs.
5h 20% 2h 40m 7d 58% 1d 7h 954.2k $4.32
| Pill | Shows |
|---|---|
| 5h | Rolling 5-hour rate limit: % used, then time until it resets |
| 7d | Weekly rate limit: % used, then time until it resets |
| | Prompt-cache tokens (read + written) since the mod loaded |
| | Session cost, the same figure as /cost |
claude plugin validate plugins/usage-band
claude plugin test plugins/usage-band
hooks/register.tsx holds the hooks (session.start, session.measure, turn.complete, and the ui.render hook for AbovePrompt); hooks/format.ts holds the pure formatting helpers.
hooks/register.tsx 73 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Limit, Tokens, Usage } from '../types'
5import { formatRemaining, formatTokens } from './format'
6
7const EMPTY_USAGE: Usage = { limits: [], costUsd: null }
8const EMPTY_TOKENS: Tokens = { cache: 0 }
9const usage = atom({ plugin: 'usage-band', key: 'usage' } as const, EMPTY_USAGE)
10const tokens = atom({ plugin: 'usage-band', key: 'tokens' } as const, EMPTY_TOKENS)
11
12const LABEL: Record<string, string> = { five_hour: '5h', seven_day: '7d' }
13const ICON: Record<string, string> = { five_hour: '', seven_day: '' }
14const TINT: Record<string, { bg: string; fg: string }> = {
15 five_hour: { bg: '#24352c', fg: '#8fbf9f' },
16 seven_day: { bg: '#2e2a3d', fg: '#b4a7e6' },
17}
18
19export const register: Register = on => {
20 on('session.start', async ($, e, next) => {
21 const now = await $.session.usage()
22 await update($, usage, () => ({ limits: now.rateLimits, costUsd: now.cost?.usd ?? null }))
23 return next(e)
24 })
25
26 on('session.measure', async ($, e, next) => {
27 await update($, usage, () => ({ limits: e.rateLimits, costUsd: e.cost?.usd ?? null }))
28 return next(e)
29 })
30
31 on('turn.complete', async ($, e, next) => {
32 const u = e.usage
33 if (u) {
34 await update($, tokens, t => ({
35 cache: t.cache + u.cache_read_input_tokens + u.cache_creation_input_tokens,
36 }))
37 }
38 return next(e)
39 })
40
41 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
42 if (e.props.hasSurvey) return next(e)
43 const { Box, Text } = $.ui.resolve(e)
44 const [{ limits, costUsd }, t, now] = await Promise.all([
45 read($, usage),
46 read($, tokens),
47 $.clock.now(),
48 ])
49
50 const pill = (key: string, bg: string, fg: string, text: string) => (
51 <Box key={key} backgroundColor={bg} paddingX={1} marginRight={1}>
52 <Text color={fg}>{text}</Text>
53 </Box>
54 )
55
56 const limitPill = (l: Limit) => {
57 const remaining = l.resetsAt ? Date.parse(l.resetsAt) - now : null
58 const tint = TINT[l.kind] ?? { bg: '#33302a', fg: '#d8b56a' }
59 const reset = remaining === null ? '' : ` ${formatRemaining(remaining)}`
60 const label = `${ICON[l.kind] ?? ''} ${LABEL[l.kind] ?? l.kind} ${Math.round(l.percentUsed)}%`
61 return pill(`limit-${l.kind}`, tint.bg, tint.fg, label + reset)
62 }
63
64 return (
65 <Box flexDirection="row" flexWrap="wrap">
66 {limits.map(limitPill)}
67 {pill('cache', '#262c40', '#8fa3e6', ` ${formatTokens(t.cache)}`)}
68 {costUsd === null ? null : pill('cost', '#36311f', '#d8b56a', ` $${costUsd.toFixed(2)}`)}
69 </Box>
70 )
71 })
72}
73hooks/format.ts 18 lines1/** 954213 -> "954.2k", 1234567 -> "1.2M", 950 -> "950" */
2export const formatTokens = (n: number): string => {
3 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
4 if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`
5 return String(n)
6}
7
8/** Time left as its two largest units: "1d 7h", "2h 40m", "12m" */
9export const formatRemaining = (ms: number): string => {
10 const minutes = Math.max(0, Math.floor(ms / 60_000))
11 const d = Math.floor(minutes / 1440)
12 const h = Math.floor((minutes % 1440) / 60)
13 const m = minutes % 60
14 if (d > 0) return `${d}d ${h}h`
15 if (h > 0) return `${h}h ${m}m`
16 return `${m}m`
17}
18types/index.d.ts 12 lines1export type Tokens = { cache: number }
2
3export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
4
5export type Usage = { limits: Limit[]; costUsd: number | null }
6
7declare module 'claude-code' {
8 interface PluginState {
9 'usage-band': { usage: Usage; tokens: Tokens }
10 }
11}
12