SLOPSHOPPER

usage-band

提示框上方顯示 token 用量與訂閱額度剩餘

newbandtoast
v0.2.0no licenseupdated 2026-10-07ArnoldChiou/claude-usage-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-band
› 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 ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ✦ Claude 用量 本次花費 $0.42 │ │ ◉ Context ██████████░░░░░░░░░░ 49% 97k / 200k │ │ Σ Token 輸入 97k(快取讀 91k・寫 4k) 輸出 1k · 1 回合 │ │ ◷ 5 小時額度 ██████░░░░░░░░░░░░░░ 剩 69% ↻ 09:53 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ✦ Claude 用量 本次花費 $0.42 │ │ ◉ Context ██████████░░░░░░░░░░ 49% 97k / 200k │ │ Σ Token 輸入 97k(快取讀 91k・寫 4k) 輸出 1k · 1 回合 │ │ ◷ 5 小時額度 ██████░░░░░░░░░░░░░░ 剩 69% ↻ 09:53 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
README

usage-band

Claude Code 的 mod:在提示框上方顯示一個用量面板。

╭──────────────────────────────────────────────────────────╮
│ ✦ Claude 用量                            本次花費 $1.23  │
│ ◉ Context     ████▓░░░░░░░░░░░░░░░  23%    45k / 200k    │
│ Σ Token       輸入 1.2M(快取讀 1.1M・寫 80k)  輸出 42k · 18 回合 │
│ ◷ 5 小時額度  ███████░░░░░░░░░░░░░  剩 65%  ↻ 13:00      │
│ ◫ 7 天額度    ██████████████████░░  剩 7.5% ↻ 10/9 08:00 │
╰──────────────────────────────────────────────────────────╯
  • Context:目前對話的 token 用量與模型上限
  • Token:本次 session 累計的輸入(含快取讀寫)與輸出 token、回合數(含子代理,/clear 後歸零)
  • 訂閱額度:5 小時/7 天額度剩餘百分比與重置時間(需以訂閱帳號登入)
  • 本次 session 估計花費
  • 額度用量達 90% 時跳出提示

安裝

在 Claude Code 的提示框輸入:

/plugin install usage-band --marketplace ArnoldChiou/claude-usage-band

出現 Add marketplace? 時按 y,再選安裝範圍(user 範圍會在之後每個 session 都啟用)。

開發

claude plugin validate .
claude plugin test .
Source 2 files
hooks/register.tsx 193 lines
1import { atom, read, update } from 'claude-code'
2import type { Register, SessionContextUsage, SessionCost, SessionRateLimit } from 'claude-code'
3
4import type { Tokens, Usage } from '../types'
5
6const usage = atom({ plugin: 'usage-band', key: 'usage' } as const, null)
7const EMPTY: Tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, since: 0 }
8const tokens = atom({ plugin: 'usage-band', key: 'tokens' } as const, EMPTY)
9
10const LABELS: Record<string, { name: string; icon: string; color: string }> = {
11  five_hour: { name: '5 小時額度', icon: '◷', color: '#5FD7FF' },
12  seven_day: { name: '7 天額度', icon: '◫', color: '#AF87FF' },
13  spend_limit: { name: '花費上限', icon: '◈', color: '#FFAF5F' },
14}
15
16const BAR = 20
17
18// 剩餘量的漸層:綠 → 黃綠 → 黃 → 橘 → 紅(依格子位置上色)
19const HEAT = ['#5FD787', '#87D75F', '#AFD75F', '#D7D75F', '#FFD75F', '#FFAF5F', '#FF875F', '#FF5F5F']
20// Context 用藍紫漸層
21const COOL = ['#5FAFFF', '#5F87FF', '#875FFF', '#AF5FFF', '#D75FFF']
22
23const toUsage = (context: SessionContextUsage, rateLimits: SessionRateLimit[], cost?: SessionCost): Usage => ({
24  tokens: context.tokens,
25  window: context.window,
26  percent: context.percent,
27  limits: rateLimits.map(r => ({ kind: r.kind, percentUsed: r.percentUsed, resetsAt: r.resetsAt })),
28  usd: cost?.usd,
29})
30
31const fmtTokens = (n: number) =>
32  n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`
33
34// 重置時間:今天顯示 HH:MM,其他日子顯示 M/D HH:MM
35const fmtReset = (iso: string, now: number) => {
36  const d = new Date(iso)
37  if (Number.isNaN(d.getTime())) return ''
38  const hm = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
39  return new Date(now).toDateString() === d.toDateString() ? hm : `${d.getMonth() + 1}/${d.getDate()} ${hm}`
40}
41
42const pick = (palette: string[], ratio: number) =>
43  palette[Math.min(palette.length - 1, Math.floor(ratio * palette.length))] ?? palette[0]!
44
45const levelColor = (used: number) => (used >= 90 ? '#FF5F5F' : used >= 70 ? '#FFAF5F' : '#5FD787')
46
47export const register: Register = on => {
48  on('session.start', async ($, e, next) => {
49    const r = await next(e)
50    const u = await $.session.usage()
51    await update($, usage, () => toUsage(u.context, u.rateLimits, u.cost))
52    // 熱重載也會觸發 session.start,startedAt 沒變就保留累計
53    await update($, tokens, t => (t.since === u.startedAt ? t : { ...EMPTY, since: u.startedAt }))
54    return r
55  })
56
57  // 每個 turn(含子代理)結束時累加它的 token 用量
58  on('turn.complete', async ($, e, next) => {
59    const r = await next(e)
60    const t = e.usage
61    if (t) {
62      const { startedAt } = await $.session.usage()
63      await update($, tokens, prev => {
64        const base = prev.since === startedAt ? prev : { ...EMPTY, since: startedAt }
65        return {
66          ...base,
67          input: base.input + t.input_tokens,
68          output: base.output + t.output_tokens,
69          cacheRead: base.cacheRead + t.cache_read_input_tokens,
70          cacheWrite: base.cacheWrite + t.cache_creation_input_tokens,
71          turns: base.turns + 1,
72        }
73      })
74    }
75    return r
76  })
77
78  on('session.measure', async ($, e, next) => {
79    await update($, usage, () => toUsage(e.context, e.rateLimits, e.cost))
80    if (e.changed.includes('rateLimits')) {
81      for (const r of e.rateLimits) {
82        if (r.percentUsed >= 90) $.ui.toast(`${LABELS[r.kind]?.name ?? r.kind}已用 ${r.percentUsed}%`)
83      }
84    }
85    return next(e)
86  })
87
88  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
89    const u = await read($, usage)
90    if (e.props.hasSurvey || u === null) return next(e)
91    const tk = await read($, tokens)
92
93    const { Box, Text } = $.ui.resolve(e)
94    const now = await $.clock.now()
95
96    // filled 格依位置套漸層,其餘為暗色底
97    const bar = (id: string, ratio: number, palette: string[]) => {
98      const filled = Math.round(Math.max(0, Math.min(1, ratio)) * BAR)
99      return (
100        <Box key={id}>
101          {Array.from({ length: BAR }, (_, i) =>
102            i < filled ? (
103              <Text key={`${id}-${i}`} color={pick(palette, i / BAR)}>
104                █
105              </Text>
106            ) : (
107              <Text key={`${id}-${i}`} color="#3A3A3A">
108                ░
109              </Text>
110            ),
111          )}
112        </Box>
113      )
114    }
115
116    const label = (icon: string, name: string, color: string) => (
117      <Box width={13}>
118        <Text color={color} bold>
119          {`${icon} ${name}`}
120        </Text>
121      </Box>
122    )
123
124    const ctxPct = u.percent ?? 0
125
126    return (
127      <Box flexDirection="column" borderStyle="round" borderColor="#875FFF" paddingX={1}>
128        <Box justifyContent="space-between">
129          <Text color="#D75FFF" bold>
130            ✦ Claude 用量
131          </Text>
132          {u.usd !== undefined ? (
133            <Text>
134              <Text color="#8A8A8A">本次花費 </Text>
135              <Text color="#FFD75F" bold>{`$${u.usd.toFixed(2)}`}</Text>
136            </Text>
137          ) : null}
138        </Box>
139
140        <Box columnGap={1}>
141          {label('◉', 'Context', '#5FAFFF')}
142          {bar('ctx', ctxPct / 100, COOL)}
143          <Box width={8}>
144            <Text color={levelColor(ctxPct)} bold>{` ${ctxPct}%`}</Text>
145          </Box>
146          <Text color="#8A8A8A">
147            {u.tokens === undefined ? `— / ${fmtTokens(u.window)}` : `${fmtTokens(u.tokens)} / ${fmtTokens(u.window)}`}
148          </Text>
149        </Box>
150
151        <Box columnGap={1}>
152          {label('Σ', 'Token', '#5FD7AF')}
153          {tk.turns === 0 ? (
154            <Text color="#8A8A8A" italic>
155              尚無回應
156            </Text>
157          ) : (
158            <Text>
159              <Text color="#8A8A8A">輸入 </Text>
160              <Text color="#87D7FF" bold>{fmtTokens(tk.input + tk.cacheRead + tk.cacheWrite)}</Text>
161              <Text color="#8A8A8A">{`(快取讀 ${fmtTokens(tk.cacheRead)}・寫 ${fmtTokens(tk.cacheWrite)})  輸出 `}</Text>
162              <Text color="#FFD787" bold>{fmtTokens(tk.output)}</Text>
163              <Text color="#8A8A8A">{`  · ${tk.turns} 回合`}</Text>
164            </Text>
165          )}
166        </Box>
167
168        {u.limits.length === 0 ? (
169          <Text color="#8A8A8A" italic>
170            ◌ 尚無訂閱額度資料(收到第一次回應後顯示)
171          </Text>
172        ) : null}
173
174        {u.limits.map(l => {
175          const meta = LABELS[l.kind] ?? { name: l.kind, icon: '◇', color: '#D7D7D7' }
176          const left = Math.max(0, Math.round((100 - l.percentUsed) * 10) / 10)
177          const reset = l.resetsAt ? fmtReset(l.resetsAt, now) : ''
178          return (
179            <Box key={l.kind} columnGap={1}>
180              {label(meta.icon, meta.name, meta.color)}
181              {bar(l.kind, l.percentUsed / 100, HEAT)}
182              <Box width={8}>
183                <Text color={levelColor(l.percentUsed)} bold>{`剩 ${left}%`}</Text>
184              </Box>
185              {reset ? <Text color="#8A8A8A">{`↻ ${reset}`}</Text> : null}
186            </Box>
187          )
188        })}
189      </Box>
190    )
191  })
192}
193
types/index.d.ts 25 lines
1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2export type Usage = {
3  tokens?: number
4  window: number
5  percent?: number
6  limits: Limit[]
7  usd?: number
8}
9// 本次 session 累計的 token 用量(每個 turn 的 usage 加總,含子代理)
10export type Tokens = {
11  input: number
12  output: number
13  cacheRead: number
14  cacheWrite: number
15  turns: number
16  // 累計起點(session 的 startedAt),/clear 後會變,用來歸零
17  since: number
18}
19
20declare module 'claude-code' {
21  interface PluginState {
22    'usage-band': { usage: Usage | null; tokens: Tokens }
23  }
24}
25