Model, context window and rate-limit (5h / 7d) braille meters in a band above the prompt

A Claude Code mod that keeps your usage in view: the model, how full the context window is, and the 5-hour and 7-day rate-limit windows, drawn as braille meters in a band above the prompt.
Opus 5.5 │ ctx ⣷ 11% │ 5h ⣿⣤ 18% ↻ 16:10 │ 7d ⣿⣿⣦ 33%
Each bar is colored by how full it is, green through yellow to red. The 5h window also shows when it resets: HH:MM within the next 20 hours, M/D HH:MM beyond that, in local time.
/plugin marketplace add kawase1295/usage-meter
/plugin install usage-meter@usage-meter
The plugin is installed over HTTPS from the released main branch. To move to a newer release later, refresh the catalog and then update the plugin, and restart Claude Code:
/plugin marketplace update usage-meter
claude plugin update usage-meter@usage-meter
ctx are shown.hooks/register.tsx draws the AbovePrompt band. Each draw reads $.session.usage() and $.session.model(); a session.measure event (after each turn, or when a rate-limit window moves a point) asks for a redraw.hooks/format.ts holds the pure parts: the braille bar, the color gradient, the reset time and the model display name (claude-opus-5-5 → Opus 5.5).scripts/check # claude plugin validate + claude plugin test
claude --plugin-dir . # run a session with this checkout loaded
MIT
hooks/register.tsx 41 lines1import type { Register } from 'claude-code'
2
3import { buildMeters, modelLabel } from './format'
4
5export const register: Register = on => {
6 // the band reads usage on every draw; a new measurement asks for a redraw
7 on('session.measure', ($, e, next) => {
8 $.ui.invalidate('ui.render')
9 return next(e)
10 })
11
12 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
13 if (e.props.hasSurvey) return next(e)
14
15 const { context, rateLimits } = await $.session.usage()
16 const model = modelLabel(await $.session.model())
17 const meters = buildMeters({ contextPercent: context.percent, rateLimits }, await $.clock.now())
18
19 const { Box, Text } = $.ui.resolve(e)
20 // Other mods beneath draw in the same band: keep theirs, the meters closest to the prompt.
21 const below = await next(e)
22
23 return (
24 <Box flexDirection="column">
25 {below}
26 <Box>
27 <Text>{model}</Text>
28 {meters.map(m => (
29 <Text key={m.label}>
30 <Text dimColor> │ {m.label} </Text>
31 <Text color={m.color}>{m.bar}</Text>
32 <Text> {m.percent}%</Text>
33 {m.reset ? <Text dimColor> ↻ {m.reset}</Text> : null}
34 </Text>
35 ))}
36 </Box>
37 </Box>
38 )
39 })
40}
41hooks/format.ts 82 lines1import type { SessionRateLimit } from 'claude-code'
2
3const BRAILLE = ' ⣀⣄⣤⣦⣶⣷⣿'
4
5export type Usage = {
6 contextPercent?: number
7 rateLimits: SessionRateLimit[]
8}
9
10export type Meter = {
11 label: string
12 percent: number
13 bar: string
14 color: string
15 reset: string
16}
17
18const clamp = (pct: number) => Math.min(Math.max(pct, 0), 100)
19
20export const brailleBar = (pct: number, width = 8): string => {
21 const level = clamp(pct) / 100
22 let bar = ''
23 for (let i = 0; i < width; i++) {
24 const start = i / width
25 const end = (i + 1) / width
26 if (level >= end) bar += BRAILLE[7]
27 else if (level <= start) bar += BRAILLE[0]
28 else bar += BRAILLE[Math.min(Math.floor(((level - start) / (end - start)) * 7), 7)]
29 }
30 return bar
31}
32
33const hex = (n: number) => Math.max(0, Math.min(255, Math.trunc(n))).toString(16).padStart(2, '0')
34
35/** green (0%) -> yellow (50%) -> red (100%) */
36export const gradient = (pct: number): string => {
37 const p = clamp(pct)
38 return p < 50 ? `#${hex(p * 5.1)}c850` : `#ff${hex(200 - (p - 50) * 4)}3c`
39}
40
41const pad2 = (n: number) => String(n).padStart(2, '0')
42
43/** Local HH:MM within 20h, else M/D HH:MM; '' when absent or unparsable. */
44export const formatReset = (iso: string | undefined, nowMs: number): string => {
45 if (!iso) return ''
46 const at = new Date(iso)
47 const ms = at.getTime()
48 if (Number.isNaN(ms)) return ''
49 const hm = `${pad2(at.getHours())}:${pad2(at.getMinutes())}`
50 const delta = ms - nowMs
51 return delta >= 0 && delta < 20 * 3600_000 ? hm : `${at.getMonth() + 1}/${at.getDate()} ${hm}`
52}
53
54/** claude-opus-5-5 -> Opus 5.5; a date suffix and [1m] are dropped. */
55export const modelLabel = (id: string): string => {
56 const m = /^claude-([a-z]+)-(\d+)-(\d+)(?:-\d{8})?(?:\[\w+\])?$/.exec(id)
57 if (!m) return id
58 const [, family = '', major, minor] = m
59 return `${family.charAt(0).toUpperCase()}${family.slice(1)} ${major}.${minor}`
60}
61
62const meter = (label: string, pct: number, reset = ''): Meter => ({
63 label,
64 percent: Math.round(pct),
65 bar: brailleBar(pct),
66 color: gradient(pct),
67 reset,
68})
69
70export const buildMeters = (u: Usage, nowMs: number): Meter[] => {
71 const meters: Meter[] = []
72 if (u.contextPercent !== undefined) meters.push(meter('ctx', u.contextPercent))
73
74 const five = u.rateLimits.find(r => r.kind === 'five_hour')
75 if (five) meters.push(meter('5h', five.percentUsed, formatReset(five.resetsAt, nowMs)))
76
77 const week = u.rateLimits.find(r => r.kind === 'seven_day')
78 if (week) meters.push(meter('7d', week.percentUsed))
79
80 return meters
81}
82