輸入框上方顯示 5h/7d 額度、重置倒數、token 與花費

A collection of Claude Code plugins (mods) that add live status displays to the interface.
Displays session usage above the prompt: 5-hour and 7-day rate-limit consumption with reset countdowns, token counts, and session cost.

When you send out helpers (subagents), a pixel office opens in the side pane. Each helper sits at a desk and randomly types, talks on the phone, rushes to the copier, drinks coffee, or thinks. Finished helpers raise their arms. Interrupted helpers turn grey and fall asleep at their desk.
![]()
/helpersclaude plugin marketplace add mikekuo0725/usage-band
claude plugin install usage-band@usage-band
claude plugin install pixel-helpers@usage-band
本專案收錄 Claude Code 外掛(mod),為介面加入即時狀態顯示。
於輸入框上方即時顯示本次工作階段的用量資訊,包括 5 小時與 7 天額度使用率、重置倒數、token 用量及花費。

派小幫手時,右邊面板會顯示一間像素辦公室。每位小幫手坐在自己的辦公桌前,隨機打字、講電話、衝去影印、喝咖啡或想事情。做完的小幫手會舉起雙手;被打斷的會變灰、趴在桌上睡著。
![]()
/helpers 打開claude plugin marketplace add mikekuo0725/usage-band
claude plugin install usage-band@usage-band
claude plugin install pixel-helpers@usage-bandhooks/register.tsx 168 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
3
4import type { Usage } from '../types'
5import { barCells, formatCount, formatCountdown, formatUsd, hasReset } from './format'
6import { addTurn } from './tokens'
7import { addReading, chart, forecastFor, trendWord } from './weather'
8
9const tokens = atom({ plugin: 'usage-band', key: 'tokens' } as const, null)
10const usage = atom({ plugin: 'usage-band', key: 'usage' } as const, null)
11const now = atom({ plugin: 'usage-band', key: 'now' } as const, 0)
12const readings = atom({ plugin: 'usage-band', key: 'readings' } as const, [])
13
14const BAR_WIDTH = 8
15
16const COLORS = {
17 fiveHour: { fg: '#3f7d58', bg: '#dcebe0' },
18 sevenDay: { fg: '#6b4fa8', bg: '#e6def5' },
19 input: { fg: '#b4483c', bg: '#f5dcd8' },
20 output: { fg: '#3f7d58', bg: '#dcebe0' },
21 cache: { fg: '#4a5fb0', bg: '#dde2f5' },
22 cost: { fg: '#8a6a1f', bg: '#f3e7c8' },
23 stale: { fg: '#6b6b6b', bg: '#e6e6e6' },
24} as const
25
26type Tone = (typeof COLORS)[keyof typeof COLORS]
27
28export const register: Register = on => {
29 on('session.start', async ($, e, next) => {
30 const u = await $.session.usage()
31 await update($, usage, () => toUsage(u.rateLimits, u.cost?.usd))
32 const ms = await $.clock.now()
33 await update($, now, () => ms)
34 await takeReading($)
35 $.clock.every(60_000, () => {
36 void $.clock.now().then(ms => update($, now, () => ms))
37 })
38 return next(e)
39 })
40
41 on('session.measure', async ($, e, next) => {
42 await update($, usage, () => toUsage(e.rateLimits, e.cost?.usd))
43 const ms = await $.clock.now()
44 await update($, now, () => ms)
45 return next(e)
46 })
47
48 on('session.end', async ($, e, next) => {
49 if (e.reason === 'clear') {
50 await update($, tokens, () => null)
51 await update($, readings, () => [])
52 }
53 return next(e)
54 })
55
56 on('turn.complete', async ($, e, next) => {
57 const result = await next(e)
58 const u = result.usage
59 if (u) {
60 // /clear 不會觸發 session.start,所以每輪都比對 startedAt 決定要不要歸零
61 const { startedAt } = await $.session.usage()
62 await update($, tokens, t => addTurn(t, startedAt, u))
63 }
64 // 記憶空間只看主對話,不看子代理
65 if (!e.agentId) await takeReading($)
66 return result
67 })
68
69 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
70 if (e.props.hasSurvey) return next(e)
71
72 const u = await read($, usage)
73 const t = await read($, tokens)
74 const nowMs = await read($, now)
75 const list = await read($, readings)
76 const { Box, Text } = $.ui.resolve(e)
77
78 const pill = (key: string, tone: Tone, children: JSX.Element) => (
79 <Box key={key} backgroundColor={tone.bg} paddingX={1} marginRight={1}>
80 {children}
81 </Box>
82 )
83
84 const limitPill = (limit: SessionRateLimit, label: string, tone: Tone) => {
85 if (hasReset(limit.resetsAt, nowMs)) {
86 const grey = COLORS.stale
87 return pill(
88 label,
89 grey,
90 <Text color={grey.fg}>
91 {label} <Text dimColor>{'░'.repeat(BAR_WIDTH)}</Text> 已重置
92 </Text>,
93 )
94 }
95 const filled = barCells(limit.percentUsed, BAR_WIDTH)
96 const countdown = formatCountdown(limit.resetsAt, nowMs)
97 return pill(
98 label,
99 tone,
100 <Text color={tone.fg}>
101 {label} {'█'.repeat(filled)}
102 <Text dimColor>{'░'.repeat(BAR_WIDTH - filled)}</Text>{' '}
103 <Text bold>{Math.round(limit.percentUsed)}%</Text>
104 {countdown ? ` │ ↻ ${countdown}` : ''}
105 </Text>,
106 )
107 }
108
109 const pills: JSX.Element[] = []
110 const five = u?.rateLimits.find(r => r.kind === 'five_hour')
111 const seven = u?.rateLimits.find(r => r.kind === 'seven_day')
112 if (five) pills.push(limitPill(five, '5h', COLORS.fiveHour))
113 if (seven) pills.push(limitPill(seven, '7d', COLORS.sevenDay))
114 if (t && t.input + t.output + t.cache > 0) {
115 pills.push(pill('in', COLORS.input, <Text color={COLORS.input.fg}>↑ {formatCount(t.input)}</Text>))
116 pills.push(pill('out', COLORS.output, <Text color={COLORS.output.fg}>↓ {formatCount(t.output)}</Text>))
117 pills.push(pill('cache', COLORS.cache, <Text color={COLORS.cache.fg}>≋ {formatCount(t.cache)}</Text>))
118 }
119 if (u?.costUsd != null) {
120 pills.push(pill('cost', COLORS.cost, <Text color={COLORS.cost.fg}>{formatUsd(u.costUsd)}</Text>))
121 }
122
123 const rows: JSX.Element[] = []
124 const latest = list[list.length - 1]
125 if (latest) {
126 const f = forecastFor(latest.percent)
127 const trend = trendWord(list)
128 const wide = (e.props.bodyColumns ?? 80) >= 60
129 rows.push(
130 <Box key="weather" flexDirection="row" paddingX={1}>
131 <Text color={f.color} bold>{`${f.icon} ${f.word}`}</Text>
132 <Text>{` ${latest.percent}% of context`}</Text>
133 <Text dimColor>{` ${formatCount(latest.tokens)} / ${formatCount(latest.window)}`}</Text>
134 {wide ? <Text dimColor>{' last turns '}</Text> : ''}
135 {wide ? <Text color={f.color}>{chart(list)}</Text> : ''}
136 {wide && trend ? <Text dimColor>{` ${trend}`}</Text> : ''}
137 </Box>,
138 )
139 }
140 if (pills.length > 0) {
141 rows.push(<Box key="usage" flexDirection="row" flexWrap="wrap">{pills}</Box>)
142 }
143
144 // 其他 mod 畫的東西(如果有)放最上面
145 const above = await next(e)
146 if (rows.length === 0) return above
147 return (
148 <Box flexDirection="column">
149 {above ?? ''}
150 {rows}
151 </Box>
152 )
153 })
154}
155
156// 讀一次記憶空間用量,加進天氣歷史
157async function takeReading($: EngineInterface) {
158 const { context } = await $.session.usage()
159 if (!context.window) return
160 const used = context.tokens ?? 0
161 const percent = Math.round(context.percent ?? (used / context.window) * 100)
162 await update($, readings, list => addReading(list, { tokens: used, window: context.window, percent }))
163}
164
165function toUsage(rateLimits: SessionRateLimit[], costUsd: number | undefined): Usage {
166 return { rateLimits, costUsd: costUsd ?? null }
167}
168hooks/format.ts 41 lines1const MIN = 60_000
2const HOUR = 60 * MIN
3const DAY = 24 * HOUR
4
5// 15600 → "15.6k";先四捨五入到一位小數再決定單位,避免出現 "1000.0k"
6export function formatCount(n: number): string {
7 if (n < 1000) return String(Math.round(n))
8 const k = Math.round(n / 100) / 10
9 if (k < 1000) return `${k.toFixed(1)}k`
10 return `${(Math.round(n / 100_000) / 10).toFixed(1)}M`
11}
12
13// 離 resetsAt 還有多久;沒時間或時間壞掉回空字串
14export function formatCountdown(resetsAt: string | undefined, nowMs: number): string {
15 if (!resetsAt) return ''
16 const at = Date.parse(resetsAt)
17 if (Number.isNaN(at)) return ''
18 const left = at - nowMs
19 if (left < MIN) return '<1m'
20 if (left >= DAY) return `${Math.floor(left / DAY)}d ${Math.floor((left % DAY) / HOUR)}h`
21 if (left >= HOUR) return `${Math.floor(left / HOUR)}h ${Math.floor((left % HOUR) / MIN)}m`
22 return `${Math.floor(left / MIN)}m`
23}
24
25export function formatUsd(usd: number): string {
26 return `$${usd.toFixed(2)}`
27}
28
29// 進度條要填幾格,夾在 0..width
30export function barCells(percent: number, width: number): number {
31 const cells = Math.round((percent / 100) * width)
32 return Math.max(0, Math.min(width, cells))
33}
34
35// 重置時間已經過了:手上的 % 是舊的,要等下一次回話才有新數字
36export function hasReset(resetsAt: string | undefined, nowMs: number): boolean {
37 if (!resetsAt) return false
38 const at = Date.parse(resetsAt)
39 return !Number.isNaN(at) && at <= nowMs
40}
41hooks/tokens.ts 15 lines1import type { ModelUsage } from 'claude-code'
2
3import type { Tokens } from '../types'
4
5// 把一輪的用量加進總數;startedAt 不同代表 /clear 過,從 0 重新算
6export function addTurn(t: Tokens | null, startedAt: number, u: ModelUsage): Tokens {
7 const base = t && t.startedAt === startedAt ? t : { startedAt, input: 0, output: 0, cache: 0 }
8 return {
9 startedAt,
10 input: base.input + u.input_tokens,
11 output: base.output + u.output_tokens,
12 cache: base.cache + u.cache_read_input_tokens + u.cache_creation_input_tokens,
13 }
14}
15hooks/weather.ts 44 lines1// 「記憶空間天氣」:改寫自 Anthropic 的 token-weather 範例 mod
2// (Copyright 2026 Anthropic PBC, Apache-2.0)
3
4import type { Reading } from '../types'
5import { formatCount } from './format'
6
7const HISTORY = 12
8const BARS = '▁▂▃▄▅▆▇█'
9
10// 依記憶空間用掉幾 % 決定天氣
11const FORECAST = [
12 { upTo: 25, icon: '☀', word: 'Clear', color: 'yellow' },
13 { upTo: 50, icon: '☁', word: 'Cloudy', color: 'cyan' },
14 { upTo: 75, icon: '☂', word: 'Showers', color: 'blue' },
15 { upTo: 90, icon: '☇', word: 'Storm', color: 'magenta' },
16 { upTo: Infinity, icon: '↯', word: 'Compact soon', color: 'red' },
17] as const
18
19export type Forecast = (typeof FORECAST)[number]
20
21export function forecastFor(percent: number): Forecast {
22 return FORECAST.find(f => percent < f.upTo) ?? FORECAST[FORECAST.length - 1]!
23}
24
25// 加一筆讀數;有真的讀數後丟掉一開始的 0,只留最後 12 筆
26export function addReading(list: Reading[], next: Reading): Reading[] {
27 const kept = list.filter(r => r.tokens > 0)
28 return [...kept, next].slice(-HISTORY)
29}
30
31// 長條依最大那筆縮放,用多少都看得出變化
32export function chart(list: Reading[]): string {
33 const top = Math.max(...list.map(r => r.tokens), 1)
34 return list.map(r => BARS[Math.min(BARS.length - 1, Math.floor((r.tokens / top) * (BARS.length - 1)))]).join('')
35}
36
37export function trendWord(list: Reading[]): string {
38 if (list.length < 2) return ''
39 const delta = list[list.length - 1]!.tokens - list[list.length - 2]!.tokens
40 if (delta > 0) return `▲ +${formatCount(delta)} last turn`
41 if (delta < 0) return `▼ ${formatCount(-delta)} last turn`
42 return 'steady'
43}
44types/index.d.ts 11 lines1export type RateLimit = { kind: string; percentUsed: number; resetsAt?: string }
2export type Tokens = { startedAt: number; input: number; output: number; cache: number }
3export type Usage = { rateLimits: RateLimit[]; costUsd: number | null }
4export type Reading = { tokens: number; window: number; percent: number }
5
6declare module 'claude-code' {
7 interface PluginState {
8 'usage-band': { tokens: Tokens | null; usage: Usage | null; now: number; readings: Reading[] }
9 }
10}
11