SLOPSHOPPER

hud

HUD above the prompt: model, project and git, context, and 5h and weekly limits.

newbandprocess
v0.1.2MITupdated 2026-10-10mboisvertdupras/hud
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · hud
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM [Opus 5.5] │ app git:(feat/auth-refresh) Context █████░░░░░ 49% │ Usage ███░░░░░░░ 31% (NaNh / 5h) ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
[Opus 5.5] │ app git:(feat/auth-refresh) Context █████░░░░░ 49% │ Usage ███░░░░░░░ 31% (NaNh / 5h)
README

hud

A heads-up display for Claude Code, drawn as a band above the prompt. It is a rebuild of claude-hud as a Claude Code mod: it reads session data from the engine directly, so there is no statusLine script and no transcript parsing.

[Opus 5.5] │ my-app git:(main*)
Context █████░░░░░ 45% │ Usage ██░░░░░░░░ 25% (1h 30m / 5h) │ Weekly ████░░░░░░ 40% (3d 4h / 7d)
  • Session line: the model, the project folder, and the git branch (with * when the working tree has changes).
  • Usage line: a context bar (green, then yellow from 70%, then red from 85%), plus the 5-hour and weekly rate limits with the time until each resets.

To collapse the band, press ctrl+x ctrl+a or select [-].

Install

In a Claude Code terminal session, run:

/plugin install hud --marketplace mboisvertdupras/hud

Answer y to add the marketplace, then choose a scope.

If you use claude-hud, disable it and remove its statusLine entry from ~/.claude/settings.json. Otherwise both HUDs show.

Develop

claude --plugin-dir /path/to/hud
claude plugin validate /path/to/hud
claude plugin test /path/to/hud

Credits

The layout, symbols and color thresholds follow claude-hud by Jarrod Watts (MIT).

License

MIT

Source 2 files
hooks/register.tsx 158 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderChildren, SessionUsage } from 'claude-code'
3
4import type { HudSession, HudUsage, HudWindow } from '../types'
5
6const session = atom({ plugin: 'hud', key: 'session' } as const, null)
7const usage = atom({ plugin: 'hud', key: 'usage' } as const, null)
8const now = atom({ plugin: 'hud', key: 'now' } as const, 0)
9
10const displayModel = (model: string) => {
11  const match = /^claude-([a-z]+)-(\d+(?:-\d\b)?)/.exec(model)
12  if (!match?.[1] || !match[2]) return model
13  return `${match[1][0]?.toUpperCase()}${match[1].slice(1)} ${match[2].replace('-', '.')}`
14}
15
16const contextColor = (percent: number) => (percent >= 85 ? 'ansi256(1)' : percent >= 70 ? 'ansi256(3)' : 'ansi256(2)')
17
18const quotaColor = (percent: number) => (percent >= 90 ? 'ansi256(1)' : percent >= 75 ? 'ansi256(13)' : 'ansi256(12)')
19
20const bar = (percent: number, width: number) => {
21  const filled = Math.round((Math.min(100, Math.max(0, percent)) / 100) * width)
22  return ['█'.repeat(filled), '░'.repeat(width - filled)] as const
23}
24
25const formatResetTime = (resetsAt: string, at: number) => {
26  const minutes = Math.ceil((Date.parse(resetsAt) - at) / 60000)
27  if (minutes <= 0) return ''
28  if (minutes < 60) return `${minutes}m`
29  const hours = Math.floor(minutes / 60)
30  if (hours >= 24) return hours % 24 > 0 ? `${Math.floor(hours / 24)}d ${hours % 24}h` : `${Math.floor(hours / 24)}d`
31  return minutes % 60 > 0 ? `${hours}h ${minutes % 60}m` : `${hours}h`
32}
33
34const toUsage = ({ context, rateLimits }: Pick<SessionUsage, 'context' | 'rateLimits'>): HudUsage => {
35  const windowOf = (kind: string) => {
36    const limit = rateLimits.find(each => each.kind === kind)
37    return limit ? { percent: limit.percentUsed, resetsAt: limit.resetsAt } : null
38  }
39  return { contextPercent: context.percent ?? 0, fiveHour: windowOf('five_hour'), sevenDay: windowOf('seven_day') }
40}
41
42async function readSession($: EngineInterface): Promise<HudSession> {
43  const cwd = await $.session.cwd()
44  const [model, branch, status] = await Promise.all([
45    $.session.model(),
46    $.process.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd }),
47    $.process.run(['git', '--no-optional-locks', 'status', '--porcelain'], { cwd }),
48  ])
49  return {
50    model: displayModel(model),
51    project: cwd.split('/').filter(Boolean).pop() ?? '/',
52    git: branch.exitCode === 0 ? { branch: branch.stdout.trim(), isDirty: status.stdout.trim() !== '' } : null,
53  }
54}
55
56async function markNow($: EngineInterface) {
57  const at = await $.clock.now()
58  await update($, now, () => at)
59  return at
60}
61
62export const register: Register = on => {
63  on('session.start', async ($, e, next) => {
64    const started = await next(e)
65    const [current, figures] = await Promise.all([readSession($), $.session.usage(), markNow($)])
66    await update($, session, () => current)
67    await update($, usage, () => toUsage(figures))
68    return started
69  })
70
71  on('session.measure', async ($, e, next) => {
72    await markNow($)
73    await update($, usage, () => toUsage(e))
74    return next(e)
75  })
76
77  on('turn.complete', async ($, e, next) => {
78    const completed = await next(e)
79    if (e.agentId === undefined) {
80      const current = await readSession($)
81      await update($, session, () => current)
82    }
83    return completed
84  })
85
86  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
87    if (e.props.hasSurvey) return next(e)
88
89    const [current, figures, at] = await Promise.all([read($, session), read($, usage), read($, now)])
90    if (!current && !figures) return next(e)
91
92    const { Box, Text } = $.ui.resolve(e)
93    const width = e.props.bodyColumns
94    const barWidth = width >= 100 ? 10 : width >= 60 ? 6 : 4
95
96    const lines: [string, RenderChildren][] = []
97    if (current) {
98      lines.push([
99        'session',
100        [
101          <Text color="ansi256(6)">[{current.model}]</Text>,
102          <Text> │ </Text>,
103          <Text color="ansi256(3)">{current.project}</Text>,
104          current.git && (
105            <Text>
106              {' '}
107              <Text color="ansi256(5)">git:(</Text>
108              <Text color="ansi256(6)">
109                {current.git.branch}
110                {current.git.isDirty ? '*' : ''}
111              </Text>
112              <Text color="ansi256(5)">)</Text>
113            </Text>
114          ),
115        ],
116      ])
117    }
118    if (figures) {
119      const [contextFilled, contextEmpty] = bar(figures.contextPercent, barWidth)
120      const quotaSegment = (label: string, window: HudWindow | null, windowLabel: string) => {
121        if (!window) return null
122        const reset = window.resetsAt ? formatResetTime(window.resetsAt, at) : ''
123        return (
124          <Text>
125            <Text> │ </Text>
126            <Text dimColor>{label} </Text>
127            <Text color={quotaColor(window.percent)}>{bar(window.percent, barWidth)[0]}</Text>
128            <Text dimColor>{bar(window.percent, barWidth)[1]}</Text>
129            <Text color={quotaColor(window.percent)}> {window.percent}%</Text>
130            {reset && <Text> ({reset} / {windowLabel})</Text>}
131          </Text>
132        )
133      }
134      lines.push([
135        'usage',
136        [
137          <Text dimColor>Context </Text>,
138          <Text color={contextColor(figures.contextPercent)}>{contextFilled}</Text>,
139          <Text dimColor>{contextEmpty}</Text>,
140          <Text color={contextColor(figures.contextPercent)}> {figures.contextPercent}%</Text>,
141          quotaSegment('Usage', figures.fiveHour, '5h'),
142          quotaSegment('Weekly', figures.sevenDay, '7d'),
143        ],
144      ])
145    }
146
147    return (
148      <Box flexDirection="column" width={width}>
149        {lines.map(([key, line]) => (
150          <Box key={key}>
151            <Text wrap="truncate">{line}</Text>
152          </Box>
153        ))}
154      </Box>
155    )
156  })
157}
158
types/index.d.ts 15 lines
1export type HudGit = { branch: string; isDirty: boolean }
2export type HudSession = { model: string; project: string; git: HudGit | null }
3export type HudWindow = { percent: number; resetsAt?: string }
4export type HudUsage = { contextPercent: number; fiveHour: HudWindow | null; sevenDay: HudWindow | null }
5
6declare module 'claude-code' {
7  interface PluginState {
8    hud: {
9      session: HudSession | null
10      usage: HudUsage | null
11      now: number
12    }
13  }
14}
15