SLOPSHOPPER

usage-bars

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

newspinner
v0.1.0no licenseupdated 2026-10-03Addeuz/dotfiles/.claude/mods/usage-bars
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-bars
› 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ctx ██████████░░░░░░░░░░ 49% 97k / 200k tokens 5h ██████░░░░░░░░░░░░░░ 31% resets 02:53

Draws

Prompt hint
? for shortcuts ctx ██████████░░░░░░░░░░ 49% 97k / 200k tokens 5h ██████░░░░░░░░░░░░░
Source 2 files
hooks/register.tsx 116 lines
1import { 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}
116
types/index.d.ts 12 lines
1export 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