Shows context window, plan rate limits, and session cost in a band above the prompt

Claude Code 모드: 현재 모델과 effort, 컨텍스트 사용량, 요금제 한도(5시간/주간), 세션 비용을 프롬프트 위 한 줄로 보여줍니다.

Claude Code v2.1.302에서 테스트했습니다.
줄 끝의 끄기 버튼으로 접고, 남는 사용량 보기 버튼으로 다시 켭니다. /usage-panel 로도 전환됩니다 (상태는 다음 세션에도 유지). effort는 시작할 때 저장된 기본값을, /effort·/model 직후엔 바뀐 값을, 응답이 오면 실제 적용된 값을 보여줍니다.
claude plugin marketplace add bbabbico/usage-panel
claude plugin install usage-panel@usage-panel
claude plugin update usage-panel@usage-panelhooks/register.js 148 lines1const LABELS = { five_hour: '5시간', seven_day: '주간', spend_limit: '지출' }
2const BAR = 8
3
4// 0-100 as a bar of BAR cells
5function bar(percent) {
6 const filled = Math.max(0, Math.min(BAR, Math.round((percent / 100) * BAR)))
7 return '█'.repeat(filled) + '░'.repeat(BAR - filled)
8}
9
10function colorFor(percent) {
11 return percent >= 90 ? 'red' : percent >= 70 ? 'yellow' : 'green'
12}
13
14// "2h13m" from an ISO timestamp
15function untilReset(iso, now) {
16 if (!iso) return ''
17 const mins = Math.max(0, Math.round((Date.parse(iso) - now) / 60000))
18 const h = Math.floor(mins / 60)
19 const d = Math.floor(h / 24)
20 if (d > 0) return d + 'd' + (h % 24) + 'h'
21 if (h > 0) return h + 'h' + (mins % 60) + 'm'
22 return mins + 'm'
23}
24
25// Whether the person turned the band off; kept in $.store across sessions
26let isHidden = false
27// The session's effort: from settings on the first draw, from /effort and /model,
28// and from each main-loop request, which is what the model actually got
29let effort = null
30let isEffortRead = false
31
32// "claude-opus-5-5" -> "Opus 5.5", anything else as given
33function modelName(id) {
34 const m = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?!\d)/.exec(id)
35 if (!m) return id
36 return m[1][0].toUpperCase() + m[1].slice(1) + ' ' + m[2] + (m[3] ? '.' + m[3] : '')
37}
38
39// The saved default for the session's model, as /effort and /model leave it
40async function effortFromSettings($) {
41 try {
42 const model = await $.session.model()
43 const s = await $.settings.read()
44 return s.modelSettings?.[model]?.effortLevel ?? s.effortLevel ?? null
45 } catch {
46 return null
47 }
48}
49
50async function setHidden($, value) {
51 isHidden = value
52 await $.store.set('hidden', value)
53 $.ui.invalidate('ui.render')
54}
55
56export function register(on) {
57 on('session.start', async ($, e, next) => {
58 await $.command.register({ name: 'usage-panel', description: '사용량 줄 켜기/끄기' })
59 isHidden = (await $.store.get('hidden')) === true
60 // Keep the reset countdowns current between measurements
61 $.clock.every(60000, () => $.ui.invalidate('ui.render'))
62 return next(e)
63 })
64
65 // Pushed after each turn and when a rate-limit window moves a point
66 on('session.measure', async ($, e, next) => {
67 $.ui.invalidate('ui.render')
68 return next(e)
69 })
70
71 // Each request the main loop sends carries the effort it asks for
72 on('turn.step', async function* ($, e, next) {
73 if (!e.agentId && e.effort !== undefined && e.effort !== effort) {
74 effort = e.effort
75 $.ui.invalidate('ui.render')
76 }
77 return yield* next(e)
78 })
79
80 // A session-only /effort is never saved, so read the level off its answer
81 on('command.run', { command: 'effort' }, async ($, e, next) => {
82 const r = await next(e)
83 const m = /effort level to (low|medium|high|xhigh|max)\b/i.exec(r?.text ?? '')
84 effort = m ? m[1].toLowerCase() : await effortFromSettings($)
85 $.ui.invalidate('ui.render')
86 return r
87 })
88
89 // Each model keeps its own saved effort
90 on('command.run', { command: 'model' }, async ($, e, next) => {
91 const r = await next(e)
92 effort = await effortFromSettings($)
93 $.ui.invalidate('ui.render')
94 return r
95 })
96
97 on('command.run', { command: 'usage-panel' }, async ($) => {
98 await setHidden($, !isHidden)
99 return { text: isHidden ? '사용량 줄을 껐어요. /usage-panel 로 다시 켤 수 있어요.' : '사용량 줄을 켰어요.' }
100 })
101
102 // One row in the band above the prompt
103 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
104 if (e.props.hasSurvey) return next(e)
105 const { Box, Text, Button } = $.ui.resolve(e)
106 if (isHidden) {
107 return Box({
108 marginTop: 1,
109 children: [Button({ key: 'show', label: '사용량 보기', plain: true, dimColor: true, onPress: () => setHidden($, false) })],
110 })
111 }
112 // The plain call costs nothing, so read it fresh on each draw
113 const usage = await $.session.usage()
114 const now = await $.clock.now()
115
116 const meter = (key, label, percent, extra) =>
117 Box({
118 key,
119 flexDirection: 'row',
120 columnGap: 1,
121 children: [
122 Text({ dimColor: true, children: [label] }),
123 Text({ color: colorFor(percent), children: [bar(percent)] }),
124 Text({ children: [Math.round(percent) + '%' + (extra ? ' (' + extra + ')' : '')] }),
125 ],
126 })
127
128 const { context, rateLimits = [], cost } = usage
129 if (!isEffortRead) {
130 isEffortRead = true
131 effort = effort ?? (await effortFromSettings($))
132 }
133 const model = modelName(await $.session.model())
134 const items = [
135 Text({ children: [model + (effort !== null ? ' · ' + effort : '')] }),
136 meter('context', '컨텍스트', context.percent ?? 0),
137 ]
138 for (const r of rateLimits) {
139 items.push(meter('rl-' + r.kind, LABELS[r.kind] || r.kind, r.percentUsed, untilReset(r.resetsAt, now)))
140 }
141 if (cost) items.push(Text({ children: ['$' + cost.usd.toFixed(2)] }))
142 items.push(Button({ key: 'hide', label: '끄기', plain: true, dimColor: true, onPress: () => setHidden($, true) }))
143
144 // One blank row above, matching the gap Claude Code leaves below the band
145 return Box({ flexDirection: 'row', columnGap: 3, marginTop: 1, children: items })
146 })
147}
148