One-line session hud under the prompt: dir, branch, context, 5h/weekly usage limits with reset countdowns, running agents, model. Tokyo Night colours; a Nerd…

A one-line hud under the Claude Code prompt, in the spot the status line used to be:
[~/dotfiles: main*] │ Ctx ▰▱▱▱▱ 21% │ 5H ▰▰▰▱▱ 64% ↻ 2h13m │ Weekly ▰▱▱▱▱ 12% ↻ 3d4h │ Agents 2 Opus 5.5 1M
| Part | What |
|---|---|
[dir: branch*] | working directory (~ for home), git branch, * when the tree has changes |
Ctx | context window used |
5H, Weekly | usage limits, green → yellow at 50% → red at 80%, with the time until each resets |
Agents | subagents running now (yellow when any are) |
| model | the session's model, shortened (Opus 5.5 1M) |
| toggles the later pane; shown only when the later plugin is installed |
It refreshes every 5 seconds and after every turn. Colours are Tokyo Night; the and meter glyphs need a Nerd Font in your terminal.
/plugin marketplace add Visal-Sok/claude-mods
/plugin install hud@claude-mods
Optional, for the button: /plugin install later@claude-mods.
MIT
hooks/register.tsx 195 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Hud, HudMeter } from '../types'
5
6const REFRESH_MS = 5000
7const BAR_CELLS = 5
8const LIVE_AGENT = new Set(['pending', 'running', 'waiting'])
9// The mod whose /later the pane button runs; the button shows only while it is installed
10const LATER_PLUGIN = 'later'
11const MINUTE = 60_000
12const HOUR = 60 * MINUTE
13const DAY = 24 * HOUR
14// The hud line's width: the terminal's, less the footer's indent and a margin.
15const DEFAULT_COLUMNS = 120
16const EDGE_COLUMNS = 4
17const MIN_COLUMNS = 40
18
19// Tokyo Night, matching the tmux popups and the later pane.
20const THEME = {
21 fg: '#c0caf5',
22 muted: '#565f89',
23 border: '#3b4261',
24 blue: '#7aa2f7',
25 cyan: '#7dcfff',
26 green: '#9ece6a',
27 yellow: '#e0af68',
28 red: '#f7768e',
29 purple: '#bb9af7',
30} as const
31
32const hud = atom({ plugin: 'hud', key: 'hud' } as const, null)
33
34// "claude-opus-5-5[1m]" or "Opus 5.5 (1M context)" -> "Opus 5.5 1M"
35export function shortModel(model: string): string {
36 const id = model.match(/^claude-([a-z]+)-(\d+)-(\d+)(?:\[(\w+)\])?/i)
37 if (id) {
38 const [, family = '', major, minor, window] = id
39 const name = `${family.charAt(0).toUpperCase()}${family.slice(1)} ${major}.${minor}`
40 return window ? `${name} ${window.toUpperCase()}` : name
41 }
42 return model.replace(/\s*\((\S+) context\)/i, ' $1').trim()
43}
44
45export function meterColor(percent: number): string {
46 if (percent >= 80) return THEME.red
47 if (percent >= 50) return THEME.yellow
48 return THEME.green
49}
50
51export function bar(percent: number): string {
52 const filled = Math.max(0, Math.min(BAR_CELLS, Math.round((percent / 100) * BAR_CELLS)))
53 return '▰'.repeat(filled) + '▱'.repeat(BAR_CELLS - filled)
54}
55
56export function tildePath(path: string, home: string | undefined): string {
57 if (home && (path === home || path.startsWith(`${home}/`))) return `~${path.slice(home.length)}`
58 return path
59}
60
61// Time until a reset, two units at most: "2h13m", "3d4h", "9m".
62export function countdown(ms: number): string {
63 if (ms <= 0) return 'now'
64 if (ms >= DAY) return `${Math.floor(ms / DAY)}d${Math.floor((ms % DAY) / HOUR)}h`
65 if (ms >= HOUR) return `${Math.floor(ms / HOUR)}h${Math.floor((ms % HOUR) / MINUTE)}m`
66 return `${Math.max(1, Math.floor(ms / MINUTE))}m`
67}
68
69async function git($: EngineInterface, args: string[]): Promise<string | null> {
70 try {
71 const run = await $.process.run(['git', ...args])
72 return run.exitCode === 0 ? run.stdout.trim() : null
73 } catch {
74 return null
75 }
76}
77
78async function refresh($: EngineInterface) {
79 const [model, usage, cwd, home, agents, commands, branch, status] = await Promise.all([
80 $.session.model(),
81 $.session.usage(),
82 $.session.cwd(),
83 $.env.get('HOME'),
84 $.agent.list(),
85 $.command.list(),
86 git($, ['branch', '--show-current']),
87 git($, ['status', '--porcelain']),
88 ])
89 const limit = (kind: string): HudMeter | null => {
90 const window = usage.rateLimits.find(one => one.kind === kind)
91 if (window === undefined) return null
92 const resetsAt = window.resetsAt ? Date.parse(window.resetsAt) : NaN
93 return { percent: window.percentUsed, resetsAt: Number.isNaN(resetsAt) ? null : resetsAt }
94 }
95 const next: Hud = {
96 model: shortModel(model),
97 dir: tildePath(cwd, home),
98 branch: branch ?? '',
99 isDirty: (status ?? '') !== '',
100 contextPercent: usage.context.percent ?? null,
101 fiveHour: limit('five_hour'),
102 weekly: limit('seven_day'),
103 runningAgents: agents.filter(agent => LIVE_AGENT.has(agent.status)).length,
104 hasLater: commands.some(command => command.name === 'later' && command.plugin === LATER_PLUGIN),
105 }
106 await update($, hud, () => next)
107}
108
109async function refreshQuietly($: EngineInterface) {
110 try {
111 await refresh($)
112 } catch (error) {
113 $.ui.log(`hud: ${(error as Error).message}`)
114 }
115}
116
117export const register: Register = on => {
118 on('session.start', async ($, e, next) => {
119 await refreshQuietly($)
120 $.clock.every(REFRESH_MS, () => void refreshQuietly($))
121 return next(e)
122 })
123
124 on('turn.complete', async ($, e, next) => {
125 const result = await next(e)
126 await refreshQuietly($)
127 return result
128 })
129
130 // Draws in the old status line's place: the hud, then the engine's hint row.
131 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
132 const hint = await next(e)
133 const state = await read($, hud)
134 if (state === null) return hint
135
136 const { Box, Button, Text } = $.ui.resolve(e)
137 const now = await $.clock.now()
138 const width = Math.max(MIN_COLUMNS, (e.viewport?.columns ?? DEFAULT_COLUMNS) - EDGE_COLUMNS)
139 const sep = <Text color={THEME.border}>{' │ '}</Text>
140 const meter = (label: string, percent: number | null, resetsAt: number | null = null) =>
141 percent !== null && (
142 <Text>
143 {sep}
144 <Text color={THEME.muted}>{`${label} `}</Text>
145 <Text color={meterColor(percent)}>{`${bar(percent)} ${Math.round(percent)}%`}</Text>
146 {resetsAt !== null && <Text color={THEME.muted}>{` ↻ ${countdown(resetsAt - now)}`}</Text>}
147 </Text>
148 )
149 const agents = state.runningAgents
150
151 return (
152 <Box flexDirection="column">
153 <Box flexDirection="row" justifyContent="space-between" gap={2} width={width}>
154 <Box flexShrink={1}>
155 <Text wrap="truncate-end">
156 <Text color={THEME.muted}>{'['}</Text>
157 <Text color={THEME.cyan}>{state.dir}</Text>
158 {state.branch !== '' && (
159 <Text>
160 <Text color={THEME.muted}>{': '}</Text>
161 <Text color={THEME.blue}>{state.branch}</Text>
162 {state.isDirty && <Text color={THEME.yellow}>{'*'}</Text>}
163 </Text>
164 )}
165 <Text color={THEME.muted}>{']'}</Text>
166 {meter('Ctx', state.contextPercent)}
167 {meter('5H', state.fiveHour?.percent ?? null, state.fiveHour?.resetsAt)}
168 {meter('Weekly', state.weekly?.percent ?? null, state.weekly?.resetsAt)}
169 {sep}
170 <Text color={THEME.muted}>{'Agents '}</Text>
171 <Text color={agents > 0 ? THEME.yellow : THEME.muted} bold={agents > 0}>
172 {String(agents)}
173 </Text>
174 </Text>
175 </Box>
176 <Box key="actions" flexDirection="row" flexShrink={0} gap={1}>
177 {state.hasLater && (
178 <Button
179 key="later"
180 plain
181 hover={{ color: THEME.blue }}
182 onPress={() => $.command.run({ command: 'later' })}
183 >
184 <Text color={THEME.purple}>{''}</Text>
185 </Button>
186 )}
187 <Text color={THEME.purple} bold>{state.model}</Text>
188 </Box>
189 </Box>
190 {hint}
191 </Box>
192 )
193 })
194}
195types/index.d.ts 21 lines1export type HudMeter = { percent: number; resetsAt: number | null }
2
3export type Hud = {
4 model: string
5 dir: string
6 branch: string
7 isDirty: boolean
8 contextPercent: number | null
9 fiveHour: HudMeter | null
10 weekly: HudMeter | null
11 runningAgents: number
12 // The later plugin is installed, so the pane button has a /later to run
13 hasLater: boolean
14}
15
16declare module 'claude-code' {
17 interface PluginState {
18 hud: { hud: Hud | null }
19 }
20}
21