Always show both the 5-hour and weekly Claude usage, with colored progress bars, above the prompt

在 Claude Code 輸入框上方,固定顯示「5 小時」與「每週」兩個用量額度。

需要支援 mod(外掛 hooks)的 Claude Code 版本,終端機版與桌面版 Code 分頁都可用。
以下兩行指令要在「終端機」執行(Mac 內建的「終端機」App,或任何終端機),不是貼到 Claude Code 的對話框。一次貼一行,按 Enter。
claude plugin marketplace add paulpc2/claude-code-mods
claude plugin install usage-both@paulpc2-mods
/reload-plugins,或完全關閉後重新開啟 Claude Code。輸入框上方出現兩條用量,就代表成功。安裝時看到「1 userConfig option not yet set」可以忽略,那是語言選項,預設是繁體中文。
預設是繁體中文。想改成英文,請照下面三個步驟做。
桌面版的外掛頁面沒有語言選項,所以要改設定檔。
open -e ~/.claude/settings.json
env 這一段:{
"env": { "USAGE_BOTH_LANGUAGE": "en" }
}
如果檔案裡已經有 env,只要在裡面多加一行 "USAGE_BOTH_LANGUAGE": "en",行與行之間記得加逗號。
Cmd + Q)。只輸入 /reload-plugins 不會生效。要改回中文,把值改成 "zh-TW",或直接刪掉那一行,一樣要完全重開。
同樣在「終端機」依序執行:
claude plugin marketplace update paulpc2-mods
claude plugin update usage-both@paulpc2-mods
更新後一樣要完全重開 Claude Code。
Shows both the 5-hour and weekly Claude usage above the Claude Code prompt.

Run the two commands below in a terminal (the macOS Terminal app or any terminal), not in the Claude Code chat box. Paste one line at a time and press Enter.
claude plugin marketplace add paulpc2/claude-code-mods
claude plugin install usage-both@paulpc2-mods
/reload-plugins in the chat box, or quit and reopen Claude Code.When two usage rows appear above the prompt, it worked. You can ignore the "1 userConfig option not yet set" note shown during install; it refers to the language option, which defaults to Traditional Chinese.
The default is Traditional Chinese. To switch to English, follow the three steps below.
The desktop app's plugin page has no language option, so the setting goes in a config file.
open -e ~/.claude/settings.json
env block:{
"env": { "USAGE_BOTH_LANGUAGE": "en" }
}
If the file already has an env block, just add "USAGE_BOTH_LANGUAGE": "en" inside it, with a comma between entries.
Cmd + Q). Running /reload-plugins alone does not apply it.To switch back, set the value to "zh-TW" or delete the line, then restart the same way.
In a terminal, run these in order:
claude plugin marketplace update paulpc2-mods
claude plugin update usage-both@paulpc2-mods
Then quit and reopen Claude Code.
MIT
hooks/register.tsx 137 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Limit } from '../types'
5
6const limits = atom({ plugin: 'usage-both', key: 'limits' } as const, [] as Limit[])
7
8const KINDS = ['five_hour', 'seven_day']
9type Lang = 'zh-TW' | 'en'
10
11const TEXT = {
12 'zh-TW': { five_hour: '5小時', seven_day: '每週', used: '已用', reset: '重置' },
13 en: { five_hour: '5h', seven_day: 'Weekly', used: 'used', reset: 'reset' },
14} as const
15
16const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
17
18const pad = (n: number) => String(n).padStart(2, '0')
19
20// 重置時間一律顯示幾點幾分(使用這台電腦的時區);一天以上才加日期
21const reset = (lang: Lang, iso?: string) => {
22 const word = TEXT[lang].reset
23 const t = iso ? Date.parse(iso) : NaN
24 if (!(t > 0)) return `--:-- ${word}`
25 const d = new Date(t)
26 const hm = `${pad(d.getHours())}:${pad(d.getMinutes())}`
27 if (t - Date.now() < 86400000) return `${hm} ${word}`
28 return lang === 'en'
29 ? `${MONTHS[d.getMonth()]} ${d.getDate()} ${hm} ${word}`
30 : `${d.getMonth() + 1}月${d.getDate()}日 ${hm} ${word}`
31}
32
33// 綠 < 60%,橘 60–79%,紅 ≥ 80%(用量);底色不透明度 80%(#CC)
34const tone = (pct: number) => (pct >= 80 ? '#D92D2DCC' : pct >= 60 ? '#E8820CCC' : '#2A9D45CC')
35
36// 進度條底色分三段(0–60、60–80、80–100),段與段之間留一點空隙當刻度(約軌道寬度 0.33%);底色用 25% 不透明
37const ZONES: [number, number, string][] = [
38 [0, 60, '#2A9D4540'],
39 [60, 80, '#E8820C40'],
40 [80, 100, '#D92D2D40'],
41]
42
43async function refresh($: any) {
44 const u = await $.session.usage()
45 const fresh: Limit[] = u.rateLimits.map((r: Limit) => ({
46 kind: r.kind,
47 percentUsed: r.percentUsed,
48 resetsAt: r.resetsAt,
49 }))
50 // 剛重新載入時還沒有新讀數:先用上次存下的數字,不顯示 0%
51 let saved: Limit[] = []
52 try {
53 saved = JSON.parse(String((await $.store.get('limits')) ?? '[]'))
54 } catch {}
55 if (fresh.length > 0) await $.store.set('limits', JSON.stringify(fresh))
56 await update($, limits, (cur: Limit[]) => {
57 const prev = cur.length > 0 ? cur : saved
58 const next = [...fresh]
59 // 視窗暫時從回報消失(例如額度用完)時,保留上一次的數字,不讓它消失
60 for (const p of prev) {
61 if (!KINDS.includes(p.kind) || next.some(n => n.kind === p.kind)) continue
62 const isExpired = p.resetsAt !== undefined && Date.parse(p.resetsAt) <= Date.now()
63 next.push(isExpired ? { kind: p.kind, percentUsed: 0 } : p)
64 }
65 return next
66 })
67}
68
69export const register: Register = (on, options) => {
70 const optionLang: Lang = options.language === 'en' ? 'en' : 'zh-TW'
71
72 on('session.start', async ($, e, next) => {
73 await refresh($)
74 return next(e)
75 })
76
77 on('session.measure', async ($, e, next) => {
78 await refresh($)
79 return next(e)
80 })
81
82 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
83 if (e.props.hasSurvey) return next(e)
84 const list = await read($, limits)
85
86 // 語言:環境變數 USAGE_BOTH_LANGUAGE 優先(桌面版不一定會把外掛設定傳進來),否則用外掛設定
87 let envLang: string | undefined
88 try {
89 envLang = await $.env.get('USAGE_BOTH_LANGUAGE')
90 } catch {}
91 const lang: Lang = envLang === 'en' || envLang === 'zh-TW' ? envLang : optionLang
92 const T = TEXT[lang]
93
94 const { Box, Text } = $.ui.resolve(e)
95 // 兩個額度永遠都畫,沒有資料時用空條佔位;5 小時固定在前
96 const rows = KINDS.map(k => list.find(l => l.kind === k) ?? { kind: k, percentUsed: 0 })
97
98 // 兩段平分整列寬度;文字(含重置時間)完整顯示,進度條撐滿剩下的空間
99 return (
100 <Box flexDirection="row" alignItems="center" width="100%">
101 {rows.map((l, i) => {
102 const used = Math.max(0, Math.min(100, l.percentUsed))
103 return (
104 <Box key={l.kind} flexDirection="row" alignItems="center" flexGrow={1} marginLeft={i === 0 ? 0 : 4}>
105 <Text bold>{T[l.kind as 'five_hour' | 'seven_day']}</Text>
106 <Text> </Text>
107 <Text bold>{T.used}</Text>
108 <Text> </Text>
109 <Text bold color="#FFFFFF" backgroundColor={tone(l.percentUsed)}> {l.percentUsed}% </Text>
110 <Text> </Text>
111 <Box flexDirection="row" flexGrow={1} height={1} alignItems="center">
112 {ZONES.flatMap(([a, b, tint], z) => {
113 const filled = Math.max(0, Math.min(used, b) - a)
114 const empty = b - a - filled
115 return [
116 // 刻度:1% 寬的格子,左邊三分之二塗上前一段的顏色,只留右邊三分之一(約 0.33%)透明
117 z > 0 && (
118 <Box key={`g${z}`} width="1%" height="60%" flexDirection="row">
119 <Box width="67%" height="100%" backgroundColor={used >= a ? tone(l.percentUsed) : ZONES[z - 1][2]} />
120 </Box>
121 ),
122 filled > 0 && <Box key={`f${z}`} flexGrow={filled} height="60%" backgroundColor={tone(l.percentUsed)} />,
123 empty > 0 && <Box key={`e${z}`} flexGrow={empty} height="60%" backgroundColor={tint} />,
124 ]
125 })}
126 </Box>
127 <Box flexShrink={0}>
128 <Text bold> {reset(lang, l.resetsAt)}</Text>
129 </Box>
130 </Box>
131 )
132 })}
133 </Box>
134 )
135 })
136}
137types/index.d.ts 8 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'usage-both': { limits: Limit[] }
6 }
7}
8