Grafický pruh s 5hodinovým a týdenním limitem a časem do resetu

Mod pro Claude Code: nad polem pro zprávu ukazuje barevný pruh s 5hodinovým a týdenním limitem předplatného a časem do resetu. Když limit dosáhne 90 %, zobrazí upozornění.
5h ████░░░░░░░░░░░░ 23% ↻ 2h 15m týden ███████░░░░░░░░░ 41% ↻ 2d 3h
V Claude Code (terminál, desktopová aplikace nebo VS Code):
/plugin install usage-limits --marketplace kbwmpmrp2z-beep/claude-usage-limits
Potvrďte y a zvolte user scope. Repozitář je soukromý, takže počítač musí mít přístup ke GitHubu (např. gh auth login).
claude update).claude plugin disable usage-limitshooks/register.tsx 117 lines1import { atom, read, update } from 'claude-code'
2import type { Register, EngineInterface, SessionRateLimit } from 'claude-code'
3
4import type { Reading } from '../types'
5
6const LABELS: Record<string, string> = { five_hour: '5h', seven_day: 'týden' }
7
8const reading = atom({ plugin: 'usage-limits', key: 'reading' } as const, { limits: [], now: 0 })
9
10export function until(resetsAt: string | undefined, now: number): string {
11 if (!resetsAt) return ''
12 const mins = Math.max(0, Math.round((Date.parse(resetsAt) - now) / 60000))
13 const d = Math.floor(mins / 1440)
14 const h = Math.floor((mins % 1440) / 60)
15 const m = mins % 60
16 if (d > 0) return `${d}d ${h}h`
17 if (h > 0) return `${h}h ${m}m`
18 return `${m}m`
19}
20
21export function windows(limits: SessionRateLimit[]): SessionRateLimit[] {
22 return limits
23 .filter(l => l.kind in LABELS)
24 .sort((a, b) => (a.kind === 'five_hour' ? -1 : b.kind === 'five_hour' ? 1 : 0))
25}
26
27export function bar(percent: number, width: number): [string, string] {
28 const filled = Math.min(width, Math.max(0, Math.round((percent / 100) * width)))
29 return ['█'.repeat(filled), '░'.repeat(width - filled)]
30}
31
32export function tone(percent: number): 'success' | 'warning' | 'error' {
33 return percent >= 90 ? 'error' : percent >= 70 ? 'warning' : 'success'
34}
35
36export const WARN_AT = 90
37
38// one warning per window per reset period: the key changes when the window resets
39export function toWarn(limits: SessionRateLimit[], warned: Set<string>): SessionRateLimit[] {
40 return windows(limits).filter(
41 l => l.percentUsed >= WARN_AT && !warned.has(`${l.kind}|${l.resetsAt ?? ''}`),
42 )
43}
44
45const warned = new Set<string>()
46
47// last reading from an earlier session: a window whose reset has passed starts over at 0 %
48export function revive(stored: unknown, now: number): SessionRateLimit[] {
49 if (!Array.isArray(stored)) return []
50 return windows(stored as SessionRateLimit[]).map(l =>
51 l.resetsAt && Date.parse(l.resetsAt) <= now ? { kind: l.kind, percentUsed: 0 } : l,
52 )
53}
54
55async function refresh($: EngineInterface, limits?: SessionRateLimit[], warn = true) {
56 const now = await $.clock.now()
57 for (const l of limits && warn ? toWarn(limits, warned) : []) {
58 warned.add(`${l.kind}|${l.resetsAt ?? ''}`)
59 const reset = until(l.resetsAt, now)
60 $.ui.toast(
61 `⚠️ Limit ${LABELS[l.kind]} je na ${Math.round(l.percentUsed)} %${reset ? ` (reset za ${reset})` : ''}`,
62 { timeoutMs: 10_000 },
63 )
64 }
65 await update($, reading, (r: Reading) => ({ limits: limits ?? r.limits, now }))
66}
67
68export const register: Register = on => {
69 on('session.start', async ($, e, next) => {
70 const result = await next(e)
71 // the first version wrote to the status line; the band replaces it
72 $.ui.status(undefined)
73 const live = (await $.session.usage()).rateLimits
74 if (windows(live).length > 0) await refresh($, live)
75 // nothing measured yet: show the last known reading until the first response
76 else await refresh($, revive(await $.store.get('last'), await $.clock.now()), false)
77 // the reset countdown moves even when no response arrives
78 $.clock.every(60_000, () => void refresh($))
79 return result
80 })
81
82 on('session.measure', async ($, e, next) => {
83 if (windows(e.rateLimits).length > 0) await $.store.set('last', e.rateLimits)
84 await refresh($, e.rateLimits)
85 return next(e)
86 })
87
88 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
89 const r = await read($, reading)
90 const shown = windows(r.limits)
91 if (e.props.hasSurvey || shown.length === 0) return next(e)
92
93 const { Box, Text } = $.ui.resolve(e)
94 const width = e.props.bodyColumns >= 90 ? 16 : 8
95
96 return (
97 <Box flexDirection="row" gap={4}>
98 {shown.map(l => {
99 const [full, empty] = bar(l.percentUsed, width)
100 const reset = until(l.resetsAt, r.now)
101 return (
102 <Box key={l.kind} flexDirection="row" gap={1}>
103 <Text bold>{LABELS[l.kind]}</Text>
104 <Text>
105 <Text color={tone(l.percentUsed)}>{full}</Text>
106 <Text dimColor>{empty}</Text>
107 </Text>
108 <Text color={tone(l.percentUsed)}>{Math.round(l.percentUsed)}%</Text>
109 {reset ? <Text dimColor>↻ {reset}</Text> : null}
110 </Box>
111 )
112 })}
113 </Box>
114 )
115 })
116}
117types/index.d.ts 9 lines1export type Window = { kind: string; percentUsed: number; resetsAt?: string }
2export type Reading = { limits: Window[]; now: number }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'usage-limits': { reading: Reading }
7 }
8}
9