SLOPSHOPPER

usage-limits

Bar above the prompt: how much of your plan limit is left (5 hours and week) and when it resets.

newbandtimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-limits
› 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 ⟨Claude Code's own drawing⟩ 5h ███████░░░ left 69% · resets in NaN min [ Hide ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ 5h ███████░░░ left 69% · resets in NaN min [ Hide ]
README

claude-mods

Function-hook mods for Claude Code (terminal and desktop). Each one adds a small piece of UI or a command to your session.

ModWhat it does
usage-limitsA bar above the prompt: plan limit left (5 hours, week) and time to reset.
proc-registry/procs command and a Processes panel: background shells and subagents with ID, port and start time, plus a counter in the status line.
context-weatherA bar above the prompt: context fill, session cost and plan limits, with a warning before compaction.

Install

claude plugin marketplace add Chronosauros/claude-mods
claude plugin install usage-limits@chronosaur-mods
claude plugin install proc-registry@chronosaur-mods
claude plugin install context-weather@chronosaur-mods

Language

English by default. For Polish, set the language option per mod (/config, or in settings.json):

{
  "pluginConfigs": {
    "usage-limits@chronosaur-mods": { "options": { "language": "pl" } },
    "proc-registry@chronosaur-mods": { "options": { "language": "pl" } },
    "context-weather@chronosaur-mods": { "options": { "language": "pl" } }
  }
}

License

MIT

Source 2 files
hooks/register.tsx 133 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import type { UsageLimits } from '../types'
4
5const limits = { plugin: 'usage-limits', key: 'limits' } as const
6const isHidden = { plugin: 'usage-limits', key: 'isHidden' } as const
7
8const BAR = 10
9const REFRESH_MS = 60_000
10
11const TEXT = {
12  en: {
13    names: { five_hour: '5h', seven_day: 'Week', spend_limit: 'Budget' } as Record<string, string>,
14    waiting: 'Limits: will show after the first reply ',
15    hide: 'Hide',
16    left: 'left',
17    reset: 'resets in',
18  },
19  pl: {
20    names: { five_hour: '5h', seven_day: 'Tydzień', spend_limit: 'Budżet' } as Record<string, string>,
21    waiting: 'Limity: pokażą się po pierwszej odpowiedzi ',
22    hide: 'Ukryj',
23    left: 'zostało',
24    reset: 'reset za',
25  },
26} as const
27
28const colorOf = (left: number) => (left <= 15 ? 'red' : left <= 35 ? 'yellow' : 'green')
29
30const bar = (left: number) => {
31  const full = Math.max(0, Math.min(BAR, Math.round((left / 100) * BAR)))
32
33  return '█'.repeat(full) + '░'.repeat(BAR - full)
34}
35
36// Relative time, so the module never depends on the sandbox's time zone.
37const countdown = (ms: number) => {
38  const minutes = Math.max(0, Math.round(ms / 60_000))
39  const days = Math.floor(minutes / 1440)
40  const hours = Math.floor((minutes % 1440) / 60)
41  const rest = minutes % 60
42
43  if (days > 0) return `${days} d ${hours} h`
44  if (hours > 0) return `${hours} h ${rest} min`
45  return `${rest} min`
46}
47
48async function measure($: EngineInterface): Promise<UsageLimits> {
49  const usage = await $.session.usage()
50  const measuredAt = await $.clock.now()
51  const windows = usage.rateLimits.map(one => ({
52    kind: one.kind,
53    percentUsed: one.percentUsed,
54    resetsAt: one.resetsAt ? Date.parse(one.resetsAt) : null,
55  }))
56
57  return { windows, measuredAt }
58}
59
60export const register: Register = (on, options) => {
61  const T = TEXT[options.language === 'pl' ? 'pl' : 'en']
62  let stop: (() => void) | null = null
63
64  on('session.start', async ($, e, next) => {
65    await $.state.set(limits, await measure($))
66    stop?.()
67    stop = $.clock.every(REFRESH_MS, () => {
68      void measure($).then(now => $.state.set(limits, now))
69    })
70
71    return next(e)
72  })
73
74  on('turn.complete', async ($, e, next) => {
75    await $.state.set(limits, await measure($))
76
77    return next(e)
78  })
79
80  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
81    const below = await next(e)
82    const { value: now = null } = await $.state.get(limits)
83    const { value: hidden = false } = await $.state.get(isHidden)
84
85    if (e.props.hasSurvey || hidden || now === null) {
86      return below
87    }
88
89    const { Box, Button, Text } = $.ui.resolve(e)
90
91    const row =
92      now.windows.length === 0 ? (
93        <Box key="usage">
94          <Text dimColor>{T.waiting}</Text>
95          <Button key="hide" label={T.hide} onPress={() => $.state.set(isHidden, true)} />
96        </Box>
97      ) : (
98        <Box key="usage">
99          {now.windows.map(one => {
100            const left = Math.max(0, Math.round(100 - one.percentUsed))
101            const reset =
102              one.resetsAt === null ? '' : ` · ${T.reset} ${countdown(one.resetsAt - now.measuredAt)}`
103
104            return (
105              <Text key={one.kind}>
106                <Text dimColor>{T.names[one.kind] ?? one.kind} </Text>
107                <Text color={colorOf(left)}>
108                  {bar(left)} {T.left} {left}%
109                </Text>
110                <Text dimColor>
111                  {reset}
112                  {'   '}
113                </Text>
114              </Text>
115            )
116          })}
117          <Button key="hide" label={T.hide} onPress={() => $.state.set(isHidden, true)} />
118        </Box>
119      )
120
121    if (!below) {
122      return row
123    }
124
125    return (
126      <Box flexDirection="column">
127        {below}
128        {row}
129      </Box>
130    )
131  })
132}
133
types/index.d.ts 10 lines
1export type UsageLimitsWindow = { kind: string; percentUsed: number; resetsAt: number | null }
2
3export type UsageLimits = { windows: UsageLimitsWindow[]; measuredAt: number } | null
4
5declare module 'claude-code' {
6  interface PluginState {
7    'usage-limits': { limits: UsageLimits; isHidden: boolean }
8  }
9}
10