SLOPSHOPPER

status-hud

A bar above the prompt showing current activity, 5-hour limit, weekly limit and context window.

newbandguardstatustimer
v1.5.1MITupdated 2026-10-04hymleong/claude-mods/plugins/status-hud
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · status-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 Idle 5H 31% NaNm 7D – CTX 49% 97K/200K ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Idle 5H 31% NaNm 7D – CTX 49% 97K/200K
README

claude-mods

Small Claude Code mods, shipped as plugins from one marketplace.

PluginWhat it does
status-hudA bar above the prompt: current activity, 5-hour limit, weekly limit and context window.
CacheA status-line countdown (Cache ⏱ 52:10) of the 1 hour prompt-cache window, counted from the last reply.

Install

In Claude Code:

/plugin marketplace add hymleong/claude-mods
/plugin install status-hud@claude-mods
/plugin install Cache@claude-mods
/reload-plugins

Each plugin installs and updates on its own. Mods need a recent Claude Code build that supports hooks modules.

Notes

  • Cache keeps its data on your machine only (in Claude Code's plugin store). It does not send anything anywhere.
  • To update later: claude plugin marketplace update claude-mods, then claude plugin update <name>@claude-mods.

License

MIT, see LICENSE.

Source 2 files
hooks/register.tsx 390 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Blocked, Call, Limit, Notice, Phase, Usage } from '../types'
5
6const usage = atom({ plugin: 'status-hud', key: 'usage' } as const, {} as Usage)
7const tick = atom({ plugin: 'status-hud', key: 'tick' } as const, 0)
8const calls = atom({ plugin: 'status-hud', key: 'calls' } as const, [] as Call[])
9const blocked = atom({ plugin: 'status-hud', key: 'blocked' } as const, [] as Blocked[])
10const phase = atom({ plugin: 'status-hud', key: 'phase' } as const, 'idle' as Phase)
11const isCompacting = atom({ plugin: 'status-hud', key: 'isCompacting' } as const, false)
12const notice = atom({ plugin: 'status-hud', key: 'notice' } as const, null)
13
14// ---------- helpers ----------
15const countdown = (iso: string | undefined, now: number) => {
16  if (!iso) return ''
17  const m = Math.max(0, Math.round((Date.parse(iso) - now) / 60000))
18  const d = Math.floor(m / 1440)
19  const h = Math.floor((m % 1440) / 60)
20  return d ? `${d}d${h}h` : h ? `${h}h${m % 60}m` : `${m}m`
21}
22
23const tokens = (n?: number) =>
24  n === undefined ? '' : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.round(n / 1e3)}K` : String(n)
25
26const fromMeasure = (e: any): Usage => {
27  const five = e.rateLimits.find((r: any) => r.kind === 'five_hour')
28  const seven = e.rateLimits.find((r: any) => r.kind === 'seven_day')
29  return {
30    ctxPct: e.context.percent,
31    ctxTokens: e.context.tokens,
32    ctxWindow: e.context.window,
33    five: five && { pct: five.percentUsed, resetsAt: five.resetsAt },
34    seven: seven && { pct: seven.percentUsed, resetsAt: seven.resetsAt },
35  }
36}
37
38const PLANNING = new Set(['TodoWrite', 'ExitPlanMode', 'EnterPlanMode', 'TaskCreate', 'TaskUpdate', 'TaskList', 'TaskGet'])
39
40const describeCall = (tool: string, input: Record<string, unknown>): { verb: string; target: string } => {
41  const path = String(input.file_path ?? input.path ?? input.notebook_path ?? '')
42  const short = path.split(/[\\/]/).slice(-2).join('/')
43  const cmd = String(input.command ?? '').replace(/\s+/g, ' ').trim()
44  if (PLANNING.has(tool)) return { verb: 'Planning', target: '' }
45  switch (tool) {
46    case 'Edit':
47    case 'Write':
48    case 'NotebookEdit':
49      return { verb: 'Editing', target: short }
50    case 'Read':
51      return { verb: 'Reading', target: short }
52    case 'Grep':
53    case 'Glob':
54      return { verb: 'Searching', target: String(input.pattern ?? '') }
55    case 'Bash':
56    case 'PowerShell':
57      return { verb: 'Running', target: cmd }
58    case 'Agent':
59    case 'Task':
60      return { verb: 'Delegating', target: String(input.description ?? '') }
61    case 'WebFetch':
62    case 'WebSearch':
63      return { verb: 'Browsing', target: String(input.url ?? input.query ?? '') }
64    default:
65      return { verb: 'Working', target: tool.replace(/^mcp__/, '').replace(/__/g, ' · ') }
66  }
67}
68
69// What the chat is doing right now, most urgent first: waiting on you, compacting, tools, writing, thinking,
70// then (for a few seconds after a turn) how the last one ended, else idle.
71type Shown = { state: string; detail: string }
72const deriveActivity = (s: { calls: Call[]; blocked: Blocked[]; isCompacting: boolean; phase: Phase; notice: Notice | null; isWorking: boolean }): Shown => {
73  if (s.blocked.length) return { state: 'Needs You', detail: s.blocked[s.blocked.length - 1].label }
74  if (s.isCompacting) return { state: 'Compacting', detail: 'context' }
75  if (s.calls.length) {
76    const latest = s.calls[s.calls.length - 1]
77    return { state: s.calls.length > 1 ? `${latest.verb} ×${s.calls.length}` : latest.verb, detail: latest.target }
78  }
79  if (s.phase === 'writing') return { state: 'Writing', detail: '' }
80  if (s.isWorking || s.phase === 'thinking') return { state: 'Thinking', detail: '' }
81  if (s.notice) {
82    return s.notice.kind === 'error'
83      ? { state: 'Error', detail: 'turn failed' }
84      : s.notice.kind === 'refused'
85        ? { state: 'Declined', detail: 'request refused' }
86        : { state: 'Interrupted', detail: '' }
87  }
88  return { state: 'Idle', detail: '' }
89}
90
91// ---------- drawing: four iOS-style items that share the full width ----------
92const FONT = "-apple-system, 'SF Pro Text', 'SF Pro Display', 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif"
93const GREEN = '#30d158'
94const AMBER = '#ff9f0a'
95const RED = '#ff453a'
96const BLUE = '#0a84ff'
97const PURPLE = '#bf5af2'
98const INDIGO = '#5e5ce6'
99const TEAL = '#64d2ff'
100const PINK = '#ff375f'
101const YELLOW = '#ffd60a'
102const GRAY = '#8e8e93'
103
104const M = 8
105const GAP = 24
106const SVG_H = 44
107const ICON = 28
108const CY = SVG_H / 2
109
110const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
111const clip = (s: string, n: number) => (n <= 1 ? '' : s.length > n ? `${s.slice(0, n - 1)}…` : s)
112const tone = (pct: number | undefined, base: string) => ((pct ?? 0) >= 90 ? RED : base)
113
114const stroke = (color: string, w = 1.5) => `fill="none" stroke="${color}" stroke-width="${w}" stroke-linecap="round" stroke-linejoin="round"`
115
116type Glyph = 'idle' | 'thinking' | 'write' | 'edit' | 'plan' | 'run' | 'read' | 'search' | 'agent' | 'web' | 'alert' | 'compact' | 'stop' | 'clock' | 'calendar' | 'window'
117
118const glyph = (kind: Glyph, cx: number, cy: number, color: string) => {
119  switch (kind) {
120    case 'idle':
121      return `<path d="M${cx - 2.6} ${cy - 4} V${cy + 4} M${cx + 2.6} ${cy - 4} V${cy + 4}" ${stroke(color, 1.8)}/>`
122    case 'thinking':
123      return `<circle cx="${cx - 4.2}" cy="${cy}" r="1.4" fill="${color}"/><circle cx="${cx}" cy="${cy}" r="1.4" fill="${color}"/><circle cx="${cx + 4.2}" cy="${cy}" r="1.4" fill="${color}"/>`
124    case 'write':
125      return `<path d="M${cx - 4.5} ${cy - 3.5} H${cx + 4.5} M${cx - 4.5} ${cy} H${cx + 4.5} M${cx - 4.5} ${cy + 3.5} H${cx + 1.5}" ${stroke(color)}/>`
126    case 'edit':
127      return `<path d="M${cx - 4} ${cy + 4.2} L${cx - 3.2} ${cy + 1} L${cx + 2.4} ${cy - 4.6} L${cx + 4.6} ${cy - 2.4} L${cx - 1} ${cy + 3.2} Z" ${stroke(color)}/>`
128    case 'plan':
129      return `<path d="M${cx - 0.5} ${cy - 3.6} H${cx + 4.6} M${cx - 0.5} ${cy} H${cx + 4.6} M${cx - 0.5} ${cy + 3.6} H${cx + 4.6}" ${stroke(color)}/><circle cx="${cx - 4}" cy="${cy - 3.6}" r="0.9" fill="${color}"/><circle cx="${cx - 4}" cy="${cy}" r="0.9" fill="${color}"/><circle cx="${cx - 4}" cy="${cy + 3.6}" r="0.9" fill="${color}"/>`
130    case 'run':
131      return `<path d="M${cx - 4.6} ${cy - 3} L${cx - 1.2} ${cy} L${cx - 4.6} ${cy + 3} M${cx + 0.8} ${cy + 3.6} H${cx + 4.6}" ${stroke(color)}/>`
132    case 'read':
133      return `<rect x="${cx - 3.8}" y="${cy - 4.8}" width="7.6" height="9.6" rx="1.6" ${stroke(color)}/><path d="M${cx - 1.6} ${cy - 1.4} H${cx + 1.6} M${cx - 1.6} ${cy + 1.4} H${cx + 1.6}" ${stroke(color, 1.2)}/>`
134    case 'search':
135      return `<circle cx="${cx - 1.2}" cy="${cy - 1.2}" r="3.4" ${stroke(color)}/><path d="M${cx + 1.4} ${cy + 1.4} L${cx + 4.6} ${cy + 4.6}" ${stroke(color)}/>`
136    case 'agent':
137      return `<circle cx="${cx}" cy="${cy - 3.6}" r="1.7" ${stroke(color, 1.3)}/><circle cx="${cx - 3.8}" cy="${cy + 3.2}" r="1.7" ${stroke(color, 1.3)}/><circle cx="${cx + 3.8}" cy="${cy + 3.2}" r="1.7" ${stroke(color, 1.3)}/>`
138    case 'web':
139      return `<circle cx="${cx}" cy="${cy}" r="4.6" ${stroke(color, 1.3)}/><path d="M${cx - 4.6} ${cy} H${cx + 4.6} M${cx} ${cy - 4.6} C${cx - 3} ${cy - 2} ${cx - 3} ${cy + 2} ${cx} ${cy + 4.6} M${cx} ${cy - 4.6} C${cx + 3} ${cy - 2} ${cx + 3} ${cy + 2} ${cx} ${cy + 4.6}" ${stroke(color, 1.1)}/>`
140    case 'alert':
141      return `<path d="M${cx} ${cy - 4.6} V${cy + 0.6}" ${stroke(color, 2)}/><circle cx="${cx}" cy="${cy + 3.8}" r="1.2" fill="${color}"/>`
142    case 'compact':
143      return `<path d="M${cx - 4} ${cy - 4.5} L${cx} ${cy - 1} L${cx + 4} ${cy - 4.5} M${cx - 4} ${cy + 4.5} L${cx} ${cy + 1} L${cx + 4} ${cy + 4.5}" ${stroke(color)}/>`
144    case 'stop':
145      return `<rect x="${cx - 3.4}" y="${cy - 3.4}" width="6.8" height="6.8" rx="1.4" fill="${color}"/>`
146    case 'clock':
147      return `<path d="M${cx} ${cy - 4.5} V${cy} L${cx + 3.4} ${cy + 2.3}" ${stroke(color, 1.3)}/>`
148    case 'calendar':
149      return `<rect x="${cx - 4.5}" y="${cy - 3.8}" width="9" height="8.2" rx="2" ${stroke(color, 1.3)}/><path d="M${cx - 4.5} ${cy - 0.8} H${cx + 4.5} M${cx - 1.9} ${cy - 5.3} V${cy - 2.6} M${cx + 1.9} ${cy - 5.3} V${cy - 2.6}" ${stroke(color, 1.3)}/>`
150    default:
151      return `<rect x="${cx - 4.9}" y="${cy - 4.1}" width="9.8" height="8.2" rx="2" ${stroke(color, 1.3)}/><path d="M${cx - 4.9} ${cy - 1.1} H${cx + 4.9}" ${stroke(color, 1.3)}/>`
152  }
153}
154
155type Look = { glyph: Glyph; color: string }
156const LOOK: Record<string, Look> = {
157  Idle: { glyph: 'idle', color: GRAY },
158  Thinking: { glyph: 'thinking', color: BLUE },
159  Writing: { glyph: 'write', color: YELLOW },
160  Editing: { glyph: 'edit', color: PURPLE },
161  Planning: { glyph: 'plan', color: INDIGO },
162  Running: { glyph: 'run', color: GREEN },
163  Reading: { glyph: 'read', color: TEAL },
164  Searching: { glyph: 'search', color: TEAL },
165  Delegating: { glyph: 'agent', color: PINK },
166  Browsing: { glyph: 'web', color: BLUE },
167  Working: { glyph: 'thinking', color: BLUE },
168  Compacting: { glyph: 'compact', color: AMBER },
169  Interrupted: { glyph: 'stop', color: GRAY },
170  Error: { glyph: 'alert', color: RED },
171  Declined: { glyph: 'alert', color: AMBER },
172  'Needs You': { glyph: 'alert', color: RED },
173}
174const lookOf = (state: string): Look => LOOK[state.replace(/ ×\d+$/, '')] ?? LOOK.Working
175
176const tint = (x: number, w: number, color: string) =>
177  `<rect x="${x - 8}" y="2" width="${w + 16}" height="${SVG_H - 4}" rx="14" fill="${color}" fill-opacity="0.14"/>`
178
179// The activity item: a tinted icon square, the item's name, the state in bold and the detail in grey.
180const activityTile = (x: number, w: number, shown: Shown) => {
181  const look = lookOf(shown.state)
182  const loud = shown.state === 'Needs You'
183  const textX = x + ICON + 12
184  const room = Math.floor((w - ICON - 12 - shown.state.length * 8.2 - 8) / 6.1)
185  return (
186    (loud ? tint(x, w, RED) : '') +
187    `<rect x="${x}" y="${CY - ICON / 2}" width="${ICON}" height="${ICON}" rx="9" fill="${look.color}" fill-opacity="0.2"/>` +
188    glyph(look.glyph, x + ICON / 2, CY, look.color) +
189    `<text x="${textX}" y="${CY - 5}" font-family="${FONT}" font-size="10" font-weight="600" fill="${loud ? RED : GRAY}">Current Activity</text>` +
190    `<text x="${textX}" y="${CY + 12}" font-family="${FONT}"><tspan font-size="15" font-weight="700" fill="${loud ? RED : '#fff'}">${esc(shown.state)}</tspan>${shown.detail && room > 3 ? `<tspan dx="8" font-size="11.5" fill="${loud ? RED : GRAY}" fill-opacity="${loud ? 0.85 : 1}">${esc(clip(shown.detail, room))}</tspan>` : ''}</text>`
191  )
192}
193
194// A usage item: a ring with a glyph, the name, then a bold percentage with its caption right-aligned.
195const usageTile = (x: number, w: number, kind: Glyph, name: string, pct: number | undefined, color: string, caption: string) => {
196  const r = 12
197  const sw = 3
198  const cx = x + ICON / 2
199  const c = 2 * Math.PI * r
200  const p = Math.max(0, Math.min(100, pct ?? 0)) / 100
201  const arc =
202    p > 0
203      ? `<circle cx="${cx}" cy="${CY}" r="${r}" fill="none" stroke="${color}" stroke-width="${sw}" stroke-linecap="round" stroke-dasharray="${(p * c).toFixed(2)} ${c.toFixed(2)}" transform="rotate(-90 ${cx} ${CY})"/>`
204      : ''
205  return (
206    `<circle cx="${cx}" cy="${CY}" r="${r}" fill="none" stroke="${color}" stroke-opacity="0.22" stroke-width="${sw}"/>` +
207    arc +
208    glyph(kind, cx, CY, color) +
209    `<text x="${x + ICON + 12}" y="${CY - 5}" font-family="${FONT}" font-size="10" font-weight="600" fill="${GRAY}">${esc(name)}</text>` +
210    `<text x="${x + ICON + 12}" y="${CY + 12}" font-family="${FONT}" font-size="15" font-weight="700" fill="#fff">${pct === undefined ? '–' : `${Math.round(pct)}%`}</text>` +
211    `<text x="${x + w}" y="${CY + 12}" text-anchor="end" font-family="${FONT}" font-size="11.5" fill="${GRAY}">${esc(caption)}</text>`
212  )
213}
214
215const hudSvg = (u: Usage, shown: Shown, now: number, width: number) => {
216  // Four items share the whole width inside the margin; the activity item gets a little more room for its detail.
217  const weights = [1.35, 1, 1, 1]
218  const total = weights.reduce((p, q) => p + q, 0)
219  const usable = width - 2 * M - 3 * GAP
220  let x = M
221  const place = (i: number) => {
222    const w = (usable * weights[i]) / total
223    const at = x
224    x += w + GAP
225    return { x: at, w }
226  }
227  const a = place(0)
228  const b = place(1)
229  const c = place(2)
230  const d = place(3)
231  // A hairline between neighbouring items, centred in the gap.
232  const divider = (t: { x: number; w: number }) => {
233    const dx = (t.x + t.w + GAP / 2).toFixed(1)
234    return `<line x1="${dx}" y1="${M}" x2="${dx}" y2="${SVG_H - M}" stroke="#fff" stroke-opacity="0.14"/>`
235  }
236  // A window whose reset time has passed is empty again; show that until the next real reading arrives.
237  const view = (l?: Limit) =>
238    l?.resetsAt && Date.parse(l.resetsAt) <= now
239      ? { pct: 0, caption: 'reset' }
240      : { pct: l?.pct, caption: l?.resetsAt ? `resets ${countdown(l.resetsAt, now)}` : '' }
241  const five = view(u.five)
242  const seven = view(u.seven)
243  const body =
244    activityTile(a.x, a.w, shown) +
245    divider(a) +
246    usageTile(b.x, b.w, 'clock', '5 Hour Limit', five.pct, tone(five.pct, GREEN), five.caption) +
247    divider(b) +
248    usageTile(c.x, c.w, 'calendar', 'Weekly Limit', seven.pct, tone(seven.pct, AMBER), seven.caption) +
249    divider(c) +
250    usageTile(d.x, d.w, 'window', 'Context Window', u.ctxPct, tone(u.ctxPct, GREEN), u.ctxWindow ? `${tokens(u.ctxTokens)} / ${tokens(u.ctxWindow)}` : '')
251  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${SVG_H}" width="${width}" height="${SVG_H}">${body}</svg>`
252}
253
254// ---------- hooks ----------
255let seq = 0
256export const register: Register = on => {
257  on('session.start', async ($, e, next) => {
258    try {
259      const u: any = await $.session.usage()
260      await update($, usage, () => fromMeasure(u))
261    } catch {}
262    // Redraw every 30s so the reset countdowns keep moving.
263    $.clock.every(30000, () => void update($, tick, n => n + 1))
264    // Clear the pinned cache line an earlier version of this plugin left under the prompt.
265    $.ui.status(undefined)
266    return next(e)
267  })
268
269  on('session.measure', async ($, e, next) => {
270    await update($, usage, () => fromMeasure(e))
271    return next(e)
272  })
273
274  on('turn.start', async ($, e, next) => {
275    await update($, notice, () => null)
276    return next(e)
277  })
278
279  // The model's own stream tells thinking from writing. Only the main conversation counts, not subagents.
280  on('turn.step', async function* ($, e, next) {
281    if (e.agentId) return yield* next(e)
282    let current: Phase = 'thinking'
283    await update($, phase, () => 'thinking')
284    for await (const chunk of next(e)) {
285      const wanted: Phase | null = chunk.kind === 'text' ? 'writing' : chunk.kind === 'thinking' || chunk.kind === 'tool' ? 'thinking' : null
286      if (wanted && wanted !== current) {
287        current = wanted
288        await update($, phase, () => wanted)
289      }
290      yield chunk
291    }
292  })
293
294  // Tools: every call in flight is tracked by id, so parallel calls neither overwrite nor clear each other.
295  on('tool.call', async ($, e, next) => {
296    const input = e as unknown as Record<string, unknown>
297    const tool = String(input.tool)
298    const id = String(input.tool_use_id ?? `c${++seq}`)
299    const isMain = !input.agentId
300
301    if (isMain) {
302      const { verb, target } = describeCall(tool, input)
303      await update($, calls, list => [...list, { id, verb, target }])
304    }
305
306    // Will the engine stop and wait for the person? Questions and plan approvals always do;
307    // for any other tool, ask the engine's own permission check. Subagent calls count too.
308    let isBlocked = tool === 'AskUserQuestion' || tool === 'ExitPlanMode'
309    const label = tool === 'AskUserQuestion' ? 'Question' : tool === 'ExitPlanMode' ? 'Approve plan' : 'Permission'
310    if (!isBlocked) {
311      try {
312        const { tool: _tool, tool_use_id: _id, ...rest } = input as any
313        const check: any = await $.tool.check({ tool, input: rest })
314        isBlocked = check?.decision === 'ask'
315      } catch {}
316    }
317    if (isBlocked) await update($, blocked, list => [...list, { id, label }])
318
319    const result = await next(e)
320
321    await update($, blocked, list => list.filter(b => b.id !== id))
322    await update($, calls, list => list.filter(c => c.id !== id))
323    return result
324  })
325
326  on('session.compact', async ($, e, next) => {
327    if (e.trigger === 'precompute' || e.agentId) return next(e)
328    await update($, isCompacting, () => true)
329    try {
330      return await next(e)
331    } finally {
332      await update($, isCompacting, () => false)
333    }
334  })
335
336  on('turn.complete', async ($, e, next) => {
337    await update($, phase, () => 'idle')
338    if (!e.agentId) {
339      await update($, calls, () => [])
340      await update($, blocked, () => [])
341      const kind = e.reason === 'error' ? 'error' : e.reason === 'refusal' ? 'refused' : e.reason === 'aborted' ? 'interrupted' : null
342      if (kind) {
343        await update($, notice, () => ({ kind }))
344        // Show how the turn ended for a few seconds, then go back to Idle.
345        $.clock.after(8000, () => void update($, notice, () => null))
346      }
347    }
348    return next(e)
349  })
350
351  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
352    if (e.props.hasSurvey) return next(e)
353    const { Box, Text } = $.ui.resolve(e)
354    const Svg = ($.ui.resolve(e) as any).Svg
355    const [u, callList, blockedList, ph, compacting, note] = await Promise.all([
356      read($, usage),
357      read($, calls),
358      read($, blocked),
359      read($, phase),
360      read($, isCompacting),
361      read($, notice),
362    ])
363    await read($, tick)
364    const now = await $.clock.now()
365    const shown = deriveActivity({ calls: callList, blocked: blockedList, isCompacting: compacting, phase: ph, notice: note, isWorking: e.props.isWorking })
366
367    if (Svg) {
368      // Fill the band: the drawing is laid out at the width of the chat column, so the items leave no gaps.
369      const width = Math.max(480, Math.min(1600, e.props.bodyColumns * 8))
370      return (
371        <Box>
372          <Svg source={hudSvg(u, shown, now, width)} width={width} height={SVG_H} alt="Current activity, 5 hour limit, weekly limit and context window" />
373        </Box>
374      )
375    }
376
377    // Terminal: one text line.
378    const pct = (n?: number) => (n === undefined ? '–' : `${Math.round(n)}%`)
379    const loud = shown.state === 'Needs You'
380    return (
381      <Box gap={3} paddingX={1}>
382        <Text color={loud ? 'red' : shown.state === 'Idle' ? undefined : 'cyan'} bold={loud}>{shown.detail ? `${shown.state} ${shown.detail}` : shown.state}</Text>
383        <Text>5H {u.five?.resetsAt && Date.parse(u.five.resetsAt) <= now ? '0% reset' : `${pct(u.five?.pct)} ${countdown(u.five?.resetsAt, now)}`}</Text>
384        <Text>7D {u.seven?.resetsAt && Date.parse(u.seven.resetsAt) <= now ? '0% reset' : `${pct(u.seven?.pct)} ${countdown(u.seven?.resetsAt, now)}`}</Text>
385        <Text>CTX {pct(u.ctxPct)} {u.ctxWindow ? `${tokens(u.ctxTokens)}/${tokens(u.ctxWindow)}` : ''}</Text>
386      </Box>
387    )
388  })
389}
390
types/index.d.ts 21 lines
1export type Limit = { pct: number; resetsAt?: string }
2export type Usage = { ctxPct?: number; ctxTokens?: number; ctxWindow?: number; five?: Limit; seven?: Limit }
3export type Call = { id: string; verb: string; target: string }
4export type Blocked = { id: string; label: string }
5export type Phase = 'idle' | 'thinking' | 'writing'
6export type Notice = { kind: 'error' | 'interrupted' | 'refused' }
7
8declare module 'claude-code' {
9  interface PluginState {
10    'status-hud': {
11      usage: Usage
12      tick: number
13      calls: Call[]
14      blocked: Blocked[]
15      phase: Phase
16      isCompacting: boolean
17      notice: Notice | null
18    }
19  }
20}
21