Five-hour, weekly and context usage bars under the prompt

hooks/register.tsx 116 lines1import { atom, read, update } from 'claude-code'
2import type { Register, SessionContextUsage, SessionRateLimit } from 'claude-code'
3
4import type { ContextFill, UsageWindow, UsageWindows } from '../types'
5
6const windows = atom({ plugin: 'usage-bars', key: 'windows' } as const, {
7 fiveHour: null,
8 sevenDay: null,
9} as UsageWindows)
10
11const context = atom({ plugin: 'usage-bars', key: 'context' } as const, null as ContextFill | null)
12
13const BAR_CELLS = 20
14const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
15
16export function toWindows(rateLimits: readonly SessionRateLimit[]): UsageWindows {
17 const find = (kind: string): UsageWindow | null => {
18 const limit = rateLimits.find(candidate => candidate.kind === kind)
19 return limit ? { percentUsed: limit.percentUsed, resetsAt: limit.resetsAt } : null
20 }
21 return { fiveHour: find('five_hour'), sevenDay: find('seven_day') }
22}
23
24export function toContextFill(usage: SessionContextUsage): ContextFill | null {
25 if (usage.tokens === undefined || usage.percent === undefined) return null
26 return { tokens: usage.tokens, window: usage.window, percent: usage.percent }
27}
28
29export function tokenText(tokens: number): string {
30 if (tokens >= 1_000_000) {
31 return `${Number((tokens / 1_000_000).toFixed(1))}M`
32 }
33 return `${Math.round(tokens / 1000)}k`
34}
35
36export function barText(percentUsed: number): string {
37 const filled = Math.round((Math.min(percentUsed, 100) / 100) * BAR_CELLS)
38 return '█'.repeat(filled) + '░'.repeat(BAR_CELLS - filled)
39}
40
41function barColor(percentUsed: number): string {
42 if (percentUsed >= 90) {
43 return 'red'
44 }
45 if (percentUsed >= 70) {
46 return 'yellow'
47 }
48 return 'green'
49}
50
51function resetText(resetsAt: string | undefined, withWeekday: boolean): string {
52 if (!resetsAt) return ''
53 const date = new Date(resetsAt)
54 const time = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
55 return withWeekday ? `resets ${WEEKDAYS[date.getDay()]} ${time}` : `resets ${time}`
56}
57
58export const register: Register = on => {
59 on('session.start', async ($, e, next) => {
60 const result = await next(e)
61 const usage = await $.session.usage()
62 await update($, windows, () => toWindows(usage.rateLimits))
63 await update($, context, () => toContextFill(usage.context))
64 return result
65 })
66
67 on('session.measure', async ($, e, next) => {
68 if (e.changed.includes('rateLimits')) {
69 await update($, windows, () => toWindows(e.rateLimits))
70 }
71 if (e.changed.includes('context')) {
72 await update($, context, () => toContextFill(e.context))
73 }
74 return next(e)
75 })
76
77 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
78 const current = await read($, windows)
79 const contextFill = await read($, context)
80 if (!current.fiveHour && !current.sevenDay && !contextFill) {
81 return next(e)
82 }
83
84 const { Box, Text } = $.ui.resolve(e)
85 const row = (label: string, window: UsageWindow | null, withWeekday: boolean) =>
86 window && (
87 <Box key={label}>
88 <Text dimColor>{label} </Text>
89 <Text color={barColor(window.percentUsed)}>{barText(window.percentUsed)}</Text>
90 <Text> {String(Math.round(window.percentUsed)).padStart(3)}% </Text>
91 <Text dimColor>{resetText(window.resetsAt, withWeekday)}</Text>
92 </Box>
93 )
94
95 return (
96 <Box flexDirection="row" flexWrap="wrap" justifyContent="space-between" columnGap={4} width="100%">
97 <Text dimColor>{e.props.hint}</Text>
98 <Box flexDirection="row" flexWrap="wrap" justifyContent="flex-end" columnGap={4}>
99 {contextFill && (
100 <Box key="ctx">
101 <Text dimColor>ctx </Text>
102 <Text color={barColor(contextFill.percent)}>{barText(contextFill.percent)}</Text>
103 <Text> {String(Math.round(contextFill.percent)).padStart(3)}% </Text>
104 <Text dimColor>
105 {tokenText(contextFill.tokens)} / {tokenText(contextFill.window)} tokens
106 </Text>
107 </Box>
108 )}
109 {row('5h', current.fiveHour, false)}
110 {row('week', current.sevenDay, true)}
111 </Box>
112 </Box>
113 )
114 })
115}
116types/index.d.ts 12 lines1export type UsageWindow = { percentUsed: number; resetsAt?: string }
2
3export type UsageWindows = { fiveHour: UsageWindow | null; sevenDay: UsageWindow | null }
4
5export type ContextFill = { tokens: number; window: number; percent: number }
6
7declare module 'claude-code' {
8 interface PluginState {
9 'usage-bars': { windows: UsageWindows; context: ContextFill | null }
10 }
11}
12