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

Small Claude Code mods, shipped as plugins from one marketplace.
| Plugin | What it does |
|---|---|
status-hud | A bar above the prompt: current activity, 5-hour limit, weekly limit and context window. |
Cache | A status-line countdown (Cache ⏱ 52:10) of the 1 hour prompt-cache window, counted from the last reply. |
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.
Cache keeps its data on your machine only (in Claude Code's plugin store). It does not send anything anywhere.claude plugin marketplace update claude-mods, then claude plugin update <name>@claude-mods.MIT, see LICENSE.
hooks/register.tsx 390 lines1import { 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, '&').replace(/</g, '<').replace(/>/g, '>')
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}
390types/index.d.ts 21 lines1export 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