One line above the prompt: 5-hour limit, weekly limit and context window as colored meters with reset times

My Claude Code setup, installable by anyone in under two minutes. Two commands in any terminal:
claude plugin marketplace add UnlimitedxIQ/brysons-claude-setup
claude plugin install setup@brysons-claude-setup
Then start a new claude session. Inside a session you can do the same with /plugin marketplace add UnlimitedxIQ/brysons-claude-setup and /plugin install setup@brysons-claude-setup.
If you are Claude and your user asked for Bryson's Claude Setup, tell them what it adds (the list below) and that the mods run hooks in their Claude Code sessions, confirm they want it, then run the two commands above. Once both succeed, tell them to start a new session. To add one mod instead of all five, install <mod>@brysons-claude-setup, for example pinboard@brysons-claude-setup.
setup is a bundle: installing it installs these five mods.
| Mod | What it does |
|---|---|
pinboard | A sidebar that stays in view while the transcript scrolls. Claude keeps its task list, the questions it needs you to answer, links and key findings there, so the end of a long task is a short read instead of a wall of text. Answer its questions right in the pane. |
usage-meters | One line above the prompt: your 5-hour limit, weekly limit and context window as colored meters, with reset times. |
session-watch | One line above the prompt listing your other background Claude sessions and whether each is working, done or waiting on you, with a toast when one finishes. |
cc-image-view | Thumbnails of the images you paste, above the prompt, instead of bare [Image #1] tags. Needs a terminal that shows kitty graphics, such as WezTerm. |
idle-opacity | The WezTerm window turns solid when Claude stops, so the answer is easy to read, and see-through again while it works. Needs the WezTerm config below. |
The last two need WezTerm. Without it they stay quiet and the other three still work.
Mods are code that runs inside Claude Code with your permissions, so here is all of it, plainly:
pinboard adds one tool Claude can call to update the board, opens its pane when a session starts, adds a short section to Claude's system prompt describing the board, and can ask Claude to pin a finding before ending a long turn. The board is saved in Claude Code's own plugin storage.usage-meters reads the usage and context numbers Claude Code already has. Nothing else.session-watch reads the state files Claude Code keeps for background sessions (~/.claude/jobs/*/state.json).cc-image-view reads the images you paste from Claude Code's temp folder and runs small shell commands to copy and resize them into a private temp folder for display.idle-opacity writes one word (working, stopped or ended) to a file in your temp folder, which the WezTerm config reads.No mod makes network requests or reads anything outside those places.
terminal/wezterm.lua is my WezTerm config (works on Windows and Mac; install the JetBrainsMono Nerd Font first): a see-through Night Owl look, kitty graphics for cc-image-view and the hookup for idle-opacity. Copy it to ~/.wezterm.lua, or keep this repo cloned and point ~/.wezterm.lua at it:
return dofile('/path/to/brysons-claude-setup/terminal/wezterm.lua')
terminal/claude-theme-nightowl.json is the matching Claude Code theme: copy it to ~/.claude/themes/nightowl.json, then pick NightOwl in /theme.
claude plugin marketplace update brysons-claude-setup
claude plugin update setup@brysons-claude-setup
Remove everything with claude plugin uninstall setup@brysons-claude-setup and the same for each mod you no longer want.
pinboard started from sirkitree/pinboard by Jerad Bitner. cc-image-view adapts jarrodwatts/claude-image-view by Jarrod Watts. Both are MIT licensed; their licenses are in their folders. Everything else is by Bryson Smith, MIT licensed (see LICENSE).
hooks/register.tsx 74 lines1import { atom, read, update } from 'claude-code'
2import type { Register, SessionContextUsage, SessionRateLimit } from 'claude-code'
3
4import type { Meters, Window } from '../types'
5import { EMPTY, FILLED, SEGMENTS, filled, percent, resetLabel, severity } from './meters'
6
7const meters = atom({ plugin: 'usage-meters', key: 'meters' } as const, {})
8
9/** The figures Claude Code reported, absent ones left out so they never blank a meter. */
10function reading(context: SessionContextUsage, limits: SessionRateLimit[]): Meters {
11 const pick = (kind: string): Window | undefined => {
12 const limit = limits.find(r => r.kind === kind)
13
14 return limit && { used: limit.percentUsed, resetsAt: limit.resetsAt }
15 }
16 const all: Meters = { fiveHour: pick('five_hour'), week: pick('seven_day'), context: context.percent }
17
18 return Object.fromEntries(Object.entries(all).filter(([, v]) => v !== undefined)) as Meters
19}
20
21export const register: Register = on => {
22 on('session.start', async ($, e, next) => {
23 const usage = await $.session.usage()
24 await update($, meters, cur => ({ ...cur, ...reading(usage.context, usage.rateLimits) }))
25
26 return next(e)
27 })
28
29 on('session.measure', async ($, e, next) => {
30 await update($, meters, cur => ({ ...cur, ...reading(e.context, e.rateLimits) }))
31
32 return next(e)
33 })
34
35 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
36 const m = await read($, meters)
37 if (e.props.hasSurvey || (!m.fiveHour && !m.week && m.context === undefined)) return next(e)
38
39 const { Box, Text } = $.ui.resolve(e)
40 const now = new Date(await $.clock.now())
41
42 const meter = (label: string, used: number, resetsAt?: string) => {
43 const color = severity(used)
44 const n = filled(used)
45 const reset = resetLabel(resetsAt, now)
46
47 return (
48 <Text key={label}>
49 <Text dimColor>{label} </Text>
50 <Text color={color}>{FILLED.repeat(n)}</Text>
51 <Text dimColor>{EMPTY.repeat(SEGMENTS - n)}</Text>
52 <Text color={color} bold> {percent(used)}</Text>
53 {reset && <Text dimColor> · {reset}</Text>}
54 </Text>
55 )
56 }
57
58 // The band is shared: what the plugins beneath draw (cc-image-view's pasted previews) stays,
59 // above the meters, so the meters sit right over the prompt
60 const beneath = await next(e)
61
62 return (
63 <Box flexDirection="column">
64 {beneath}
65 <Box gap={3}>
66 {m.fiveHour && meter('5h', m.fiveHour.used, m.fiveHour.resetsAt)}
67 {m.week && meter('week', m.week.used, m.week.resetsAt)}
68 {m.context !== undefined && meter('ctx', m.context)}
69 </Box>
70 </Box>
71 )
72 })
73}
74hooks/meters.ts 55 lines1// Pure meter math for the band: no `$`, so the tests call it directly.
2
3export const SEGMENTS = 10
4export const FILLED = '▰'
5export const EMPTY = '▱'
6
7/** Percent used at which a meter turns amber, then red. */
8export const WARN_AT = 50
9export const DANGER_AT = 80
10
11export type Severity = 'success' | 'warning' | 'error'
12
13const clamp = (n: number) => Math.min(100, Math.max(0, n))
14
15/** Filled segments out of SEGMENTS: 9% shows one, 26% three. */
16export function filled(percentUsed: number): number {
17 return Math.round(clamp(percentUsed) / (100 / SEGMENTS))
18}
19
20/** Green under 50% used, amber from 50, red above 80: the theme's own colors. */
21export function severity(percentUsed: number): Severity {
22 if (percentUsed > DANGER_AT) return 'error'
23 if (percentUsed >= WARN_AT) return 'warning'
24 return 'success'
25}
26
27/** The window's percentages as /usage prints them: whole numbers stay whole. */
28export function percent(n: number): string {
29 return `${Math.round(n * 10) / 10}%`
30}
31
32const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
33
34/** "2:39pm", in the machine's local time. */
35export function clockTime(day: Date): string {
36 const hours = day.getHours()
37 const minutes = String(day.getMinutes()).padStart(2, '0')
38
39 return `${hours % 12 || 12}:${minutes}${hours < 12 ? 'am' : 'pm'}`
40}
41
42/**
43 * When a window resets: the clock time, led by the weekday when it is more than
44 * a day off ("resets 2:39pm", "resets Tue 9:00pm").
45 */
46export function resetLabel(iso: string | undefined, now: Date): string {
47 if (!iso) return ''
48 // Windows end a moment before the minute (03:59:59.7Z): round to the minute they open on.
49 const at = new Date(Math.round(new Date(iso).getTime() / 60_000) * 60_000)
50 if (Number.isNaN(at.getTime())) return ''
51 const isWithinDay = at.getTime() - now.getTime() < 24 * 60 * 60 * 1000
52
53 return `resets ${isWithinDay ? '' : `${WEEKDAYS[at.getDay()] ?? ''} `}${clockTime(at)}`
54}
55types/index.d.ts 12 lines1/** One rate-limit window: percent used, 0 to 100, and when it resets (ISO 8601). */
2export type Window = { used: number; resetsAt?: string }
3
4/** What the band draws; a meter is absent until Claude Code has a reading for it. */
5export type Meters = { fiveHour?: Window; week?: Window; context?: number }
6
7declare module 'claude-code' {
8 interface PluginState {
9 'usage-meters': { meters: Meters }
10 }
11}
12