HUD above the prompt in the desktop Code tab: dir, running subagents, context, weekly limit, cost and cache, activity; three layouts via /hud

Claude 桌面版 Code 页的 HUD。HUD 是输入框上方的一条信息栏。
它显示:文件夹、运行中的子代理(圆点)、上下文用量、每周额度、费用、缓存、活动。 终端里不显示,终端用 Rust 版状态栏。
插件名是 hud-desktop。claude-hud 不能用:claude- 开头的名字被 Claude Code 保留。
| 编号 | 名字 | 样子 |
|---|---|---|
| 1 | 仪表盘 | 五行,没有卡片。多了累计 token 和最近一次请求 |
| 2 | 单行 | 一行,只放最要紧的 |
| 3 | 卡片 | 四张卡片(默认) |
子代理:每个运行中的子代理一个圆点,四个一组。没有子代理时不显示。
/hud 1、/hud 2、/hud 3:选一个布局。/hud:不带参数,轮换到下一个。选择存在 $.store 里,下次会话还在。
把本目录装到 ~/.claude/claude-hud/desktop,再在 ~/.claude/settings.json 的 env 里加:
{ "env": { "CLAUDE_CODE_PLUGIN_DIRS": "~/.claude/claude-hud/desktop" } }
claude plugin validate .
claude plugin test .hooks/register.tsx 223 lines1import { atom, read, update } from 'claude-code'
2import type { Register, EngineInterface } from 'claude-code'
3import type { Layout, Meter, Tokens } from '../types'
4import { add, dots, hitRate, LAYOUTS, num, pickLayout, snapshot, span } from './format'
5import { ICONS, type IconName } from './icons'
6
7const snap = atom({ plugin: 'hud-desktop', key: 'snap' } as const, null)
8const turn = atom({ plugin: 'hud-desktop', key: 'turn' } as const, null)
9const running = atom({ plugin: 'hud-desktop', key: 'running' } as const, null)
10const total = atom({ plugin: 'hud-desktop', key: 'total' } as const, null)
11// Ids of subagents started and not yet finished.
12const agents = atom({ plugin: 'hud-desktop', key: 'agents' } as const, [] as string[])
13// Mirrors $.store 'layout' so the band redraws when /hud changes it.
14const layout = atom({ plugin: 'hud-desktop', key: 'layout' } as const, 3 as Layout)
15
16// Host UTC offset in minutes, from `date +%z` (e.g. +0800 -> 480); read once.
17let offset: number | undefined
18const utcOffset = async ($: EngineInterface): Promise<number> => {
19 if (offset !== undefined) return offset
20 const r = await $.process.run(['date', '+%z']).catch(() => null)
21 const m = /^([+-])(\d\d)(\d\d)/.exec(r?.stdout.trim() ?? '')
22 offset = m ? (m[1] === '-' ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3])) : 0
23 return offset
24}
25
26// Meters already warned about, so a toast fires once per crossing of 80%.
27const warned = new Set<string>()
28
29const refresh = async ($: EngineInterface) => {
30 const cwd = await $.session.cwd()
31 const [model, usage, now, version, repo, turns] = await Promise.all([
32 $.session.model(),
33 $.session.usage({ breakdown: 'summary' }),
34 $.clock.now(),
35 $.session.version(),
36 $.session.repo(),
37 $.session.turns(),
38 ])
39 const next = snapshot(model, cwd, usage, now, { version: version.version, repo: repo?.name ?? '', turns, offsetMin: await utcOffset($) })
40 for (const m of [next.context, next.week]) {
41 if (!m) continue
42 if (m.percent >= 80 && !warned.has(m.label)) {
43 warned.add(m.label)
44 $.ui.toast(`${m.label}已用 ${m.percent}%`)
45 } else if (m.percent < 80) warned.delete(m.label)
46 }
47 await update($, snap, () => next)
48}
49
50// Green under 50%, amber under 80%, red above.
51const tone = (p: number) => (p < 50 ? 'green' : p < 80 ? 'yellow' : 'red')
52const HEX = { green: '#4ade80', yellow: '#facc15', red: '#f87171', magenta: '#e879f9' } as const
53const GRAY = '#8f8d86'
54const PURPLE = '#a78bfa'
55const clamp = (p: number) => Math.min(100, Math.max(0, p))
56
57// Rounded usage bar; with `time`, a purple hairline under it shows how far
58// through its window the limit is.
59const svgBar = (p: number, width: number, time?: number) => {
60 const w = Math.round((clamp(p) / 100) * width)
61 const h = time === undefined ? 6 : 10
62 const usage = `<rect width="${width}" height="6" rx="3" fill="#8884"/>${w ? `<rect width="${Math.max(w, 6)}" height="6" rx="3" fill="${HEX[tone(p)]}"/>` : ''}`
63 const window = time === undefined ? '' : `<rect y="8.5" width="${width}" height="1.5" rx="0.75" fill="#8883"/><rect y="8.5" width="${Math.round((clamp(time) / 100) * width)}" height="1.5" rx="0.75" fill="${PURPLE}"/>`
64 return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${h}" viewBox="0 0 ${width} ${h}">${usage}${window}</svg>`
65}
66
67export const register: Register = on => {
68 let turnStart = 0
69 let tools = 0
70
71 on('session.start', async ($, e, next) => {
72 const r = await next(e)
73 const saved = Number(await $.store.get('layout'))
74 if (saved === 1 || saved === 2 || saved === 3) await update($, layout, () => saved)
75 await $.command.register({ name: 'hud', description: '切换 HUD 布局:1 仪表盘,2 单行,3 卡片;不带参数则轮换', argumentHint: '[1|2|3]', immediate: true })
76 await refresh($).catch(() => {})
77 return r
78 })
79
80 on('command.run', { command: 'hud' }, async ($, e) => {
81 const l = pickLayout(e.args, await read($, layout))
82 if (l === null) return { text: '用法:/hud 1(仪表盘)、/hud 2(单行)、/hud 3(卡片),不带参数则轮换' }
83 await update($, layout, () => l)
84 await $.store.set('layout', l)
85 return { text: `HUD 已切换为「${LAYOUTS[l]}」` }
86 })
87
88 // Fires after each main turn and when a rate-limit window moves.
89 on('session.measure', async ($, e, next) => {
90 const r = await next(e)
91 await refresh($).catch(() => {})
92 return r
93 })
94
95 on('prompt.submit', async ($, e, next) => {
96 turnStart = await $.clock.now()
97 tools = 0
98 return next(e)
99 })
100
101 on('tool.call', async ($, e, next) => {
102 tools += 1
103 await update($, running, () => e.tool)
104 try {
105 return await next(e)
106 } finally {
107 await update($, running, () => null)
108 }
109 })
110
111 on('agent.spawn', async ($, e, next) => {
112 const r = await next(e)
113 const id = r.agentId
114 if (id) await update($, agents, prev => [...prev, id])
115 return r
116 })
117
118 // Main turns and subagent turns both count toward the session total.
119 // A subagent's loop runs as one turn, so its turn.complete means it finished.
120 on('turn.complete', async ($, e, next) => {
121 const id = e.agentId
122 if (id !== undefined) await update($, agents, prev => prev.filter(a => a !== id))
123 const u = e.usage
124 if (u) {
125 const t: Tokens = { input: u.input_tokens, output: u.output_tokens, cacheRead: u.cache_read_input_tokens, cacheWrite: u.cache_creation_input_tokens }
126 await update($, total, prev => add(prev, t))
127 }
128 if (e.agentId === undefined) {
129 const seconds = Math.round(((await $.clock.now()) - turnStart) / 1000)
130 await update($, turn, () => ({ seconds, tools }))
131 }
132 return next(e)
133 })
134
135 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
136 // The terminal has the Rust statusline; this band is for the desktop.
137 if (e.surface === 'terminal') return next(e)
138 const s = await read($, snap)
139 if (e.props.hasSurvey || s === null) return next(e)
140 const [t, tool, sum, now, mode, subs] = await Promise.all([read($, turn), read($, running), read($, total), $.clock.now(), read($, layout), read($, agents)])
141 const { Box, Text, Svg } = $.ui.resolve(e)
142
143 const icon = (name: IconName, color: keyof typeof HEX | 'gray' = 'gray') =>
144 <Svg source={`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="${color === 'gray' ? GRAY : HEX[color]}" d="${ICONS[name].path}"/></svg>`} alt={name.replace(/_/g, ' ')} width={15} height={15} />
145 const row = (...children: unknown[]) => <Box flexDirection="row" alignItems="center" columnGap={1}>{children}</Box>
146 const gauge = (m: Meter, width: number) =>
147 <Svg source={svgBar(m.percent, width, m.elapsed)} alt={`${m.label}已用 ${m.percent}%`} width={width} height={m.elapsed === undefined ? 6 : 10} />
148 const pct = (m: Meter) => <Text bold color={tone(m.percent)}>{m.percent}%</Text>
149 const dim = (s: string) => <Text dimColor>{s}</Text>
150 const card = (key: string, body: unknown) => (
151 <Box key={key} flexDirection="column" borderStyle="round" borderDimColor paddingX={1} flexGrow={1}>
152 {body}
153 </Box>
154 )
155 const d = dots(subs.length)
156 const dir = row(
157 icon('folder_outline'),
158 <Text>{s.dir}</Text>,
159 subs.length > 0 && <Box key="agents"><Svg source={`<svg xmlns="http://www.w3.org/2000/svg" width="${d.width}" height="6" viewBox="0 0 ${d.width} 6">${d.xs.map(x => `<circle cx="${x}" cy="3" r="3" fill="${PURPLE}"/>`).join('')}</svg>`} alt={`${subs.length} 个子代理运行中`} width={d.width} height={6} /></Box>,
160 )
161 const version = row(icon('tag_outline'), dim(`Claude Code ${s.version}`))
162 const cost = row(icon('currency_usd', 'yellow'), <Text color="yellow">{s.cost.replace('$', '') || '—'}</Text>)
163 const hit = (x: Tokens) => <Text bold color={hitRate(x) >= 80 ? 'green' : 'yellow'}>{hitRate(x)}%</Text>
164 const tokens = (x: Tokens) => dim(`输入 ${num(x.input)},输出 ${num(x.output)},缓存读取 ${num(x.cacheRead)},缓存写入 ${num(x.cacheWrite)}`)
165 const activity = [
166 row(icon('repeat'), <Text>第 {s.turns} 轮</Text>),
167 row(icon('clock_outline'), <Text>{span(now - s.startedAt)}</Text>),
168 t && row(icon('lightning_bolt_outline'), dim(`上一轮 ${t.seconds} 秒,工具 ${t.tools} 次`)),
169 tool ? row(icon('play', 'magenta'), <Text color="magenta">正在运行 {tool}</Text>) : row(icon('sleep'), dim('空闲')),
170 ]
171 const line = (...children: unknown[]) => <Box flexDirection="row" alignItems="center" columnGap={2} paddingX={1} flexWrap="wrap">{children}</Box>
172
173 if (mode === 1) return (
174 <Box flexDirection="column" rowGap={1}>
175 {line(dir, <Box flexGrow={1} />, cost, version)}
176 {(s.context || s.week) && line(
177 s.context && row(icon('brain'), gauge(s.context, 140), pct(s.context), dim(s.context.note)),
178 s.week && row(icon('calendar_week'), gauge(s.week, 140), pct(s.week), dim(`${s.week.resetIn}后重置`), s.week.resetAt && <Text color={PURPLE}>{s.week.resetAt}</Text>),
179 )}
180 {line(...activity)}
181 {sum && line(row(icon('database_outline'), dim('本次会话累计'), tokens(sum), dim('缓存命中率'), hit(sum)))}
182 {s.tokens && line(row(icon('swap_vertical'), dim('最近一次请求'), tokens(s.tokens)))}
183 </Box>
184 )
185
186 if (mode === 2) return line(
187 dir,
188 s.context && row(icon('brain'), gauge(s.context, 80), pct(s.context)),
189 s.week && row(icon('calendar_week'), gauge(s.week, 80), pct(s.week), dim(`${s.week.resetIn}后重置`)),
190 <Box flexGrow={1} />,
191 cost,
192 version,
193 )
194
195 return (
196 <Box flexDirection="column" rowGap={1}>
197 {line(dir, <Box flexGrow={1} />, version)}
198 <Box flexDirection="row" columnGap={1} flexWrap="wrap">
199 {s.context && card('ctx', <>
200 {row(icon('brain'), gauge(s.context, 120))}
201 {row(pct(s.context), dim(s.context.note))}
202 </>)}
203 {s.week && card('week', <>
204 {row(icon('calendar_week'), gauge(s.week, 120))}
205 {row(pct(s.week), dim(`${s.week.resetIn}后重置`))}
206 {s.week.resetAt && <Text color={PURPLE}>{s.week.resetAt}</Text>}
207 </>)}
208 {card('cost', <>
209 {cost}
210 {sum && row(icon('database_outline'), <Text>命中率</Text>, hit(sum))}
211 {sum && dim(`读取 ${num(sum.cacheRead)},写入 ${num(sum.cacheWrite)}`)}
212 </>)}
213 {card('act', <>
214 {row(activity[0], dim('·'), activity[1])}
215 {activity[2]}
216 {activity[3]}
217 </>)}
218 </Box>
219 </Box>
220 )
221 })
222}
223hooks/format.ts 94 lines1import type { SessionUsage } from 'claude-code'
2import type { Hud, Layout, Tokens } from '../types'
3
4const toMs = (at: string | number | undefined): number =>
5 at === undefined ? NaN : typeof at === 'number' || /^\d+$/.test(at) ? Number(at) * 1000 : Date.parse(at)
6
7// Milliseconds in plain Chinese: 6天17小时, 2小时13分钟, 45分钟.
8export const span = (ms: number): string => {
9 const m = Math.max(0, Math.round(ms / 60000))
10 const d = Math.floor(m / 1440), h = Math.floor((m % 1440) / 60)
11 return d ? `${d}天${h}小时` : h ? `${h}小时${m % 60}分钟` : `${m}分钟`
12}
13
14export const left = (at: string | number | undefined, now: number): string => {
15 const t = toMs(at)
16 return Number.isNaN(t) ? '' : span(t - now)
17}
18
19// Share of a window of `windowMs` already gone, 0-100; undefined when the reset is unknown.
20export const elapsed = (at: string | number | undefined, now: number, windowMs: number): number | undefined => {
21 const t = toMs(at)
22 return Number.isNaN(t) ? undefined : Math.min(100, Math.max(0, Math.round(100 - ((t - now) / windowMs) * 100)))
23}
24
25const WEEK = ['日', '一', '二', '三', '四', '五', '六']
26
27// Reset moment in local time: 10月11日 16点,星期日 (minutes shown when not :00).
28// offsetMin is the host's UTC offset; the hook runtime's own clock zone is not trusted.
29export const resetAt = (at: string | number | undefined, offsetMin: number): string => {
30 const t = toMs(at)
31 if (Number.isNaN(t)) return ''
32 const d = new Date(t + offsetMin * 60000)
33 const m = d.getUTCMinutes()
34 return `${d.getUTCMonth() + 1}月${d.getUTCDate()}日 ${d.getUTCHours()}点${m ? `${m}分` : ''},星期${WEEK[d.getUTCDay()]}`
35}
36
37// Token counts the Chinese way: 927, 1.2万, 3.4亿.
38export const num = (n: number): string =>
39 n >= 1e8 ? `${(n / 1e8).toFixed(1)}亿` : n >= 1e4 ? `${(n / 1e4).toFixed(1)}万` : String(n)
40
41// claude-opus-5-5 -> Opus 5.5; anything else unchanged.
42export const pretty = (model: string): string => {
43 const m = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?(.*)$/.exec(model)
44 return m ? `${m[1][0].toUpperCase()}${m[1].slice(1)} ${m[2]}${m[3] ? `.${m[3]}` : ''}${m[4]}` : model
45}
46
47// Cache read over everything sent as input, 0-100.
48export const hitRate = (t: Tokens): number => {
49 const all = t.cacheRead + t.cacheWrite + t.input
50 return all ? Math.round((t.cacheRead / all) * 100) : 0
51}
52
53export const add = (a: Tokens | null, b: Tokens): Tokens => ({
54 input: (a?.input ?? 0) + b.input,
55 output: (a?.output ?? 0) + b.output,
56 cacheRead: (a?.cacheRead ?? 0) + b.cacheRead,
57 cacheWrite: (a?.cacheWrite ?? 0) + b.cacheWrite,
58})
59
60export type Extra = Pick<Hud, 'version' | 'repo' | 'turns'> & { offsetMin: number }
61
62export const snapshot = (model: string, cwd: string, u: SessionUsage, now: number, x: Extra): Hud => {
63 const c = u.context
64 const w = u.rateLimits.find(r => r.kind === 'seven_day')
65 const a = c.breakdown?.apiUsage
66 return {
67 model: pretty(model),
68 dir: cwd.split('/').pop() || cwd,
69 cost: u.cost ? `$${u.cost.usd.toFixed(2)}` : '',
70 context: c.percent === undefined ? null : { label: '上下文', percent: c.percent, note: `${num(c.tokens ?? 0)} / ${num(c.window)}` },
71 week: w ? { label: '每周额度', percent: Math.round(w.percentUsed), note: '', resetIn: left(w.resetsAt, now), resetAt: resetAt(w.resetsAt, x.offsetMin), elapsed: elapsed(w.resetsAt, now, 7 * 86400000) } : null,
72 version: x.version,
73 repo: x.repo,
74 turns: x.turns,
75 startedAt: u.startedAt,
76 tokens: a ? { input: a.input_tokens, output: a.output_tokens, cacheRead: a.cache_read_input_tokens, cacheWrite: a.cache_creation_input_tokens } : null,
77 }
78}
79
80export const LAYOUTS: Record<Layout, string> = { 1: '仪表盘', 2: '单行', 3: '卡片' }
81
82// `/hud` argument to a layout: 1-3 picks one, empty cycles, anything else is null.
83export const pickLayout = (arg: string, current: Layout): Layout | null => {
84 const a = arg.trim()
85 if (!a) return ((current % 3) + 1) as Layout
86 return a === '1' || a === '2' || a === '3' ? (Number(a) as Layout) : null
87}
88
89// Subagent dots: centers of r=3 circles 4px apart, 10px between groups of 4.
90export const dots = (n: number): { xs: number[]; width: number } => {
91 const xs = Array.from({ length: n }, (_, i) => 3 + i * 10 + Math.floor(i / 4) * 6)
92 return { xs, width: n ? xs[n - 1]! + 3 : 0 }
93}
94hooks/icons.ts 18 lines1// Material Design Icons 7.4.47 (Apache-2.0): 24x24 SVG paths.
2export const ICONS = {
3 folder_outline: { path: 'M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z' },
4 brain: { path: 'M21.33,12.91C21.42,14.46 20.71,15.95 19.44,16.86L20.21,18.35C20.44,18.8 20.47,19.33 20.27,19.8C20.08,20.27 19.69,20.64 19.21,20.8L18.42,21.05C18.25,21.11 18.06,21.14 17.88,21.14C17.37,21.14 16.89,20.91 16.56,20.5L14.44,18C13.55,17.85 12.71,17.47 12,16.9C11.5,17.05 11,17.13 10.5,17.13C9.62,17.13 8.74,16.86 8,16.34C7.47,16.5 6.93,16.57 6.38,16.56C5.59,16.57 4.81,16.41 4.08,16.11C2.65,15.47 1.7,14.07 1.65,12.5C1.57,11.78 1.69,11.05 2,10.39C1.71,9.64 1.68,8.82 1.93,8.06C2.3,7.11 3,6.32 3.87,5.82C4.45,4.13 6.08,3 7.87,3.12C9.47,1.62 11.92,1.46 13.7,2.75C14.12,2.64 14.56,2.58 15,2.58C16.36,2.55 17.65,3.15 18.5,4.22C20.54,4.75 22,6.57 22.08,8.69C22.13,9.8 21.83,10.89 21.22,11.82C21.29,12.18 21.33,12.54 21.33,12.91M16.33,11.5C16.9,11.57 17.35,12 17.35,12.57A1,1 0 0,1 16.35,13.57H15.72C15.4,14.47 14.84,15.26 14.1,15.86C14.35,15.95 14.61,16 14.87,16.07C20,16 19.4,12.87 19.4,12.82C19.34,11.39 18.14,10.27 16.71,10.33A1,1 0 0,1 15.71,9.33A1,1 0 0,1 16.71,8.33C17.94,8.36 19.12,8.82 20.04,9.63C20.09,9.34 20.12,9.04 20.12,8.74C20.06,7.5 19.5,6.42 17.25,6.21C16,3.25 12.85,4.89 12.85,5.81V5.81C12.82,6.04 13.06,6.53 13.1,6.56A1,1 0 0,1 14.1,7.56C14.1,8.11 13.65,8.56 13.1,8.56V8.56C12.57,8.54 12.07,8.34 11.67,8C11.19,8.31 10.64,8.5 10.07,8.56V8.56C9.5,8.61 9.03,8.21 9,7.66C8.92,7.1 9.33,6.61 9.88,6.56C10.04,6.54 10.82,6.42 10.82,5.79V5.79C10.82,5.13 11.07,4.5 11.5,4C10.58,3.75 9.59,4.08 8.59,5.29C6.75,5 6,5.25 5.45,7.2C4.5,7.67 4,8 3.78,9C4.86,8.78 5.97,8.87 7,9.25C7.5,9.44 7.78,10 7.59,10.54C7.4,11.06 6.82,11.32 6.3,11.13C5.57,10.81 4.75,10.79 4,11.07C3.68,11.34 3.68,11.9 3.68,12.34C3.68,13.08 4.05,13.77 4.68,14.17C5.21,14.44 5.8,14.58 6.39,14.57C6.24,14.31 6.11,14.04 6,13.76C5.81,13.22 6.1,12.63 6.64,12.44C7.18,12.25 7.77,12.54 7.96,13.08C8.36,14.22 9.38,15 10.58,15.13C11.95,15.06 13.17,14.25 13.77,13C14,11.62 15.11,11.5 16.33,11.5M18.33,18.97L17.71,17.67L17,17.83L18,19.08L18.33,18.97M13.68,10.36C13.7,9.83 13.3,9.38 12.77,9.33C12.06,9.29 11.37,9.53 10.84,10C10.27,10.58 9.97,11.38 10,12.19A1,1 0 0,0 11,13.19C11.57,13.19 12,12.74 12,12.19C12,11.92 12.07,11.65 12.23,11.43C12.35,11.33 12.5,11.28 12.66,11.28C13.21,11.31 13.68,10.9 13.68,10.36Z' },
5 calendar_week: { path: 'M6 1H8V3H16V1H18V3H19C20.11 3 21 3.9 21 5V19C21 20.11 20.11 21 19 21H5C3.89 21 3 20.1 3 19V5C3 3.89 3.89 3 5 3H6V1M5 8V19H19V8H5M7 10H17V12H7V10Z' },
6 currency_usd: { path: 'M7,15H9C9,16.08 10.37,17 12,17C13.63,17 15,16.08 15,15C15,13.9 13.96,13.5 11.76,12.97C9.64,12.44 7,11.78 7,9C7,7.21 8.47,5.69 10.5,5.18V3H13.5V5.18C15.53,5.69 17,7.21 17,9H15C15,7.92 13.63,7 12,7C10.37,7 9,7.92 9,9C9,10.1 10.04,10.5 12.24,11.03C14.36,11.56 17,12.22 17,15C17,16.79 15.53,18.31 13.5,18.82V21H10.5V18.82C8.47,18.31 7,16.79 7,15Z' },
7 database_outline: { path: 'M12 3C7.58 3 4 4.79 4 7V17C4 19.21 7.59 21 12 21S20 19.21 20 17V7C20 4.79 16.42 3 12 3M18 17C18 17.5 15.87 19 12 19S6 17.5 6 17V14.77C7.61 15.55 9.72 16 12 16S16.39 15.55 18 14.77V17M18 12.45C16.7 13.4 14.42 14 12 14C9.58 14 7.3 13.4 6 12.45V9.64C7.47 10.47 9.61 11 12 11C14.39 11 16.53 10.47 18 9.64V12.45M12 9C8.13 9 6 7.5 6 7S8.13 5 12 5C15.87 5 18 6.5 18 7S15.87 9 12 9Z' },
8 repeat: { path: 'M17,17H7V14L3,18L7,22V19H19V13H17M7,7H17V10L21,6L17,2V5H5V11H7V7Z' },
9 clock_outline: { path: 'M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z' },
10 lightning_bolt_outline: { path: 'M11 9.47V11H14.76L13 14.53V13H9.24L11 9.47M13 1L6 15H11V23L18 9H13V1Z' },
11 sleep: { path: 'M23,12H17V10L20.39,6H17V4H23V6L19.62,10H23V12M15,16H9V14L12.39,10H9V8H15V10L11.62,14H15V16M7,20H1V18L4.39,14H1V12H7V14L3.62,18H7V20Z' },
12 play: { path: 'M8,5.14V19.14L19,12.14L8,5.14Z' },
13 tag_outline: { path: 'M21.41 11.58L12.41 2.58A2 2 0 0 0 11 2H4A2 2 0 0 0 2 4V11A2 2 0 0 0 2.59 12.42L11.59 21.42A2 2 0 0 0 13 22A2 2 0 0 0 14.41 21.41L21.41 14.41A2 2 0 0 0 22 13A2 2 0 0 0 21.41 11.58M13 20L4 11V4H11L20 13M6.5 5A1.5 1.5 0 1 1 5 6.5A1.5 1.5 0 0 1 6.5 5Z' },
14 swap_vertical: { path: 'M9,3L5,7H8V14H10V7H13M16,17V10H14V17H11L15,21L19,17H16Z' },
15} as const
16
17export type IconName = keyof typeof ICONS
18types/index.d.ts 25 lines1// elapsed: how far through its time window the meter is, 0-100 (rate limits only).
2export type Meter = { label: string; percent: number; note: string; elapsed?: number; resetIn?: string; resetAt?: string }
3export type Tokens = { input: number; output: number; cacheRead: number; cacheWrite: number }
4export type Hud = {
5 model: string
6 dir: string
7 cost: string
8 context: Meter | null
9 week: Meter | null
10 version: string
11 repo: string
12 turns: number
13 startedAt: number
14 tokens: Tokens | null
15}
16export type Turn = { seconds: number; tools: number }
17// 1 dashboard, 2 single row, 3 cards.
18export type Layout = 1 | 2 | 3
19
20declare module 'claude-code' {
21 interface PluginState {
22 'hud-desktop': { snap: Hud | null; turn: Turn | null; running: string | null; total: Tokens | null; agents: string[]; layout: Layout }
23 }
24}
25