SLOPSHOPPER

limits

Shows your 5h and 7d limits, and stops deep research from running on expensive models unless your 5h window is about to reset and your weekly usage is low.

panerowscommandstatuspromptmodel
v0.10.0MITupdated 2026-10-06westpoint-io/claude-limits-checker
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · limits
│ ┃ Limits settings ✕ › fix the failing auth test and add an audit log call │ ┃ Models to watch │ ┃ Deep research on these asks first, unless ⏺ Read(src/auth.ts) │ ┃ both rules below pass. ⎿ Read 6 lines │ ┃ [ ✓ Fable ] [ ○ Opus ] ⏺ Update(src/auth.ts) │ ┃ ⎿ Added 2 lines, removed 1 line │ ┃ 5h window resets within ⏺ Bash(bun test) │ ┃ 30 min ▾ ⎿ 3 pass, 1 fail │ ┃ │ ┃ Weekly usage under ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ 75% ▾ │ ┃ ✻ Worked for 42s · done 4:20 PM │ ┃ 🕙 5h 31% 09:53 │ ┃ Deep research will ask first right now. › /limits │ ┃ 🕙 5h 31% 09:53 │ ┃ [ ✓ Open this panel when a session starts ] Deep research on Fable asks first right now. You're on Opus. │ ✗ 5h resets in NaNm (needs under 30 min) │ ? Weekly usage not known yet (needs under 75%) │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ limits: 🕙 5h 31% 09:53

Draws

Pane · Limits settings
Models to watch Deep research on these asks first, unless both rules below pass. [ ✓ Fable ] [ ○ Opus ] 5h window resets within 30 min ▾ Weekly usage under 75% ▾ 🕙 5h 31% 09:53 Deep research will ask first right now. [ ✓ Open this panel when a session starts ]
Command output
🕙 5h 31% 09:53 Deep research on Fable asks first right now. You're on Opus. ✗ 5h resets in NaNm (needs under 30 min) ? Weekly usage not known yet (needs under 75%)
README

claude-limits-checker

Keeps an eye on your Claude Code usage limits, and stops deep research from burning Fable when you can't afford it.

🕤 5h 6% 21:20 | 🗓️ 7d 74% Oct 8 18:00
Deep research on Fable asks first right now. You're on Opus.
  ✗ 5h resets in 3h 12m (needs under 30 min)
  ✓ Weekly 74% used (needs under 75%)

Install

Run this in your terminal:

curl -fsSL https://raw.githubusercontent.com/westpoint-io/claude-limits-checker/main/install.sh | bash

Or paste this to Claude Code:

Install the Claude Code plugin from https://github.com/westpoint-io/claude-limits-checker by running
curl -fsSL https://raw.githubusercontent.com/westpoint-io/claude-limits-checker/main/install.sh | bash

Then restart Claude Code and run /limits.

The script installs the limits plugin. If you don't have a status line yet, it also sets up the Catppuccin one. If you already have one, it leaves it alone.

claude plugin marketplace add westpoint-io/claude-limits-checker
claude plugin install limits@claude-limits-checker

For the status line, save statusline.sh somewhere and point statusLine in ~/.claude/settings.json at it:

{ "statusLine": { "type": "command", "command": "bash ~/.claude/limits-statusline.sh" } }

What it does

Your limits

Under the prompt you always see both windows and when they reset. The clock face matches the 5h reset time.

Deep research guard

Before a deep research prompt runs on Fable, it checks two things:

  • your 5h window resets in under 30 minutes (use it or lose it)
  • your weekly usage is under 75%

If both are true, the prompt runs on Fable as normal. If not, it stops and asks:

This looks like deep research on Fable.
✗ 5h resets in 3h 12m (needs under 30 min)
✓ Weekly 74% used (needs under 75%)
Which model should run it?

  1. Sonnet
  2. Opus
  3. Keep Fable
  4. Cancel

Sonnet or Opus switches the model and sends your prompt again. Keep Fable runs it anyway. Cancel drops it.

The same check runs on research commands like the built-in /deep-research. Pick Sonnet or Opus there and it switches the model and runs the command again.

Haiku reads each prompt once to tell deep research apart from everything else. Quick questions, edits and fixes go straight through, and so does anything on a model you don't watch.

Commands

CommandWhat it does
/limitsPrints your limits and today's verdict, and opens the settings pane (it also opens by itself when a session starts)
/limits:settingsOpens just the settings pane

Settings

Open the pane with /limits and change things there. It saves straight away.

SettingDefaultMeaning
Models to watchFableFable, Opus or both
5h window resets within30 minDeep research is allowed only this close to the 5h reset
Weekly usage under75%Deep research is allowed only while the week is below this
Open on startOnOpens the settings pane with your limits whenever a session starts

You can also use /config under limits, or claude plugin configure limits@claude-limits-checker.

Requirements

  • Claude Code 2.1.288 or newer
  • A Claude subscription (Pro or Max) for the 5h and weekly numbers. Before the first response of a session there's no reading yet, so it asks to be safe.
  • For the status line: jq, git, and a truecolor terminal. gh is optional, for the PR number.

Development

claude plugin validate .
claude plugin test .

hooks/rules.ts has the rule and the status text, hooks/register.tsx the prompt check, the dialog, the commands and the settings pane.

License

MIT, see LICENSE. Free to use and change, at home or at work.

Made by Westpoint. The status line comes from our claude-status-line. Found a bug or want something added? Open an issue, PRs are welcome too.

Source 3 files
hooks/register.tsx 331 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, PromptSubmitInput, Register, SessionRateLimit } from 'claude-code'
3
4import { NAMES, WATCHABLE, allows, choicesFor, explain, familyOf, limitsLine, parseModels, readLimits } from './rules'
5import type { Family, Limits, Rule } from './rules'
6
7type $ = EngineInterface
8
9const ALIAS: Record<string, string> = { Sonnet: 'sonnet', Opus: 'opus' }
10const FAMILIES = WATCHABLE
11const PANE = 'limits-settings'
12const MINUTE_STEPS = [10, 15, 20, 30, 45, 60, 90, 120]
13const PERCENT_STEPS = [50, 60, 70, 75, 80, 85, 90, 95]
14
15/** The last thing the settings pane saved, or why it couldn't. */
16const notice = atom({ plugin: 'limits', key: 'notice' } as const, null)
17
18// Set from the user's config each time the module loads.
19let rule: Rule = { resetWithinMinutes: 30, weeklyMaxPercent: 75 }
20let watched: Set<Family> = new Set(['fable'])
21let openOnStart = true
22// The config values this load started from; pane edits saved on top of them stay valid while they match.
23let base = ''
24
25type Saved = { models: string; resetWithinMinutes: number; weeklyMaxPercent: number; openOnStart: boolean; base: string }
26
27const snapshot = () => ({
28  models: FAMILIES.filter(f => watched.has(f)).join(', '),
29  resetWithinMinutes: rule.resetWithinMinutes,
30  weeklyMaxPercent: rule.weeklyMaxPercent,
31  openOnStart,
32})
33
34const currentLimits = async ($: $): Promise<Limits> =>
35  readLimits((await $.session.usage()).rateLimits, await $.clock.now())
36
37const isDeepResearch = async ($: $, text: string): Promise<boolean> => {
38  const trimmed = text.trim()
39  if (trimmed.length < 15 || trimmed.startsWith('/')) return false
40  const label = await $.model.classify(
41    'Is this request to a coding assistant a deep research task? Deep research means open-ended ' +
42      'investigation that reads many sources (web pages, docs, papers or a large part of a codebase) ' +
43      'and writes up findings. Small questions, edits, fixes and builds are "other".\n\nRequest:\n' +
44      trimmed.slice(0, 4000),
45    ['deep-research', 'other'],
46  )
47
48  return label === 'deep-research'
49}
50
51/** Commands that run a research harness, like the built-in /deep-research. */
52const isResearchCommand = (name: string) => /research/i.test(name) && !name.startsWith('autoresearch')
53
54/** The watched model you're on, or undefined when the check doesn't apply. */
55const watchedModel = async ($: $): Promise<Family | undefined> => {
56  const current = familyOf(await $.session.model())
57  return current && watched.has(current) ? current : undefined
58}
59
60/** Applies the rule, asking when it fails: 'run', 'cancel', or the model alias to switch to. */
61const decide = async ($: $, current: Family, what: string): Promise<string> => {
62  const limits = await currentLimits($)
63  if (allows(limits, rule)) return 'run'
64  const answer = await $.ui.ask(`${what} on ${NAMES[current]}.\n${explain(limits, rule).join('\n')}\nWhich model should run it?`, {
65    options: choicesFor(current),
66    header: 'Limits',
67  })
68  if (answer === `Keep ${NAMES[current]}`) return 'run'
69
70  return ALIAS[answer] ?? 'cancel'
71}
72
73/** Switches the model, then runs the same slash command again. */
74const rerun = async ($: $, model: string, command: string, args: string) => {
75  await $.command.run({ command: 'model', args: model })
76  await $.command.run({ command, args })
77}
78
79/** Switches the model, then sends the same prompt again. Runs after the original was dropped. */
80const resend = async ($: $, model: string, e: PromptSubmitInput) => {
81  await $.command.run({ command: 'model', args: model })
82  await $.prompt.submit({ text: e.text, attachments: e.attachments, asUser: true })
83}
84
85/** A dropdown's options, with the current value kept even when it's not a preset. */
86const steps = (presets: number[], current: number, unit: string) =>
87  [...new Set([...presets, current])].sort((a, b) => a - b).map(n => ({ value: String(n), label: `${n}${unit}` }))
88
89/** Applies pane edits saved in the store, unless the config changed since they were made. */
90const loadSaved = async ($: $) => {
91  const saved = (await $.store.get('settings')) as Saved | undefined
92  if (!saved) return
93  if (saved.base !== base) return $.store.delete('settings')
94  watched = parseModels(saved.models)
95  rule = { resetWithinMinutes: saved.resetWithinMinutes, weeklyMaxPercent: saved.weeklyMaxPercent }
96  openOnStart = saved.openOnStart ?? openOnStart
97}
98
99/**
100 * Writes one setting the way /config does, and keeps it in the store too in case this
101 * install has no config row for it. A refusal (an org lock) undoes the change.
102 */
103const save = async ($: $, field: string, value: string | number | boolean, done: string, undo: () => void) => {
104  let deny: string | undefined
105  try {
106    deny = (await $.config.set({ key: `limits.${field}`, value })).deny
107  } catch {
108    // No config row for this field here: the store copy below carries it.
109  }
110  if (deny) {
111    undo()
112    return update($, notice, () => `Couldn't save: ${deny}`)
113  }
114  await $.store.set('settings', { ...snapshot(), base } satisfies Saved)
115  await update($, notice, () => done)
116}
117
118const toggleModel = async ($: $, family: Family) => {
119  const before = watched
120  const next = new Set(watched)
121  if (next.has(family)) next.delete(family)
122  else next.add(family)
123  watched = next
124  const list = FAMILIES.filter(f => next.has(f))
125  const names = list.map(f => NAMES[f]).join(', ') || 'no models'
126  await save($, 'models', list.join(', '), `Saved. Watching ${names}.`, () => (watched = before))
127}
128
129const setMinutes = async ($: $, value: string) => {
130  const before = rule
131  rule = { ...rule, resetWithinMinutes: Number(value) }
132  await save($, 'resetWithinMinutes', Number(value), `Saved. Allowed within ${value} min of the 5h reset.`, () => (rule = before))
133}
134
135const setPercent = async ($: $, value: string) => {
136  const before = rule
137  rule = { ...rule, weeklyMaxPercent: Number(value) }
138  await save($, 'weeklyMaxPercent', Number(value), `Saved. Allowed while weekly usage is under ${value}%.`, () => (rule = before))
139}
140
141const toggleOpenOnStart = async ($: $) => {
142  const before = openOnStart
143  openOnStart = !openOnStart
144  await save(
145    $,
146    'openOnStart',
147    openOnStart,
148    openOnStart ? 'Saved. This panel opens when a session starts.' : 'Saved. This panel stays closed until you run /limits.',
149    () => (openOnStart = before),
150  )
151}
152
153const openSettings = ($: $) => $.ui.open({ id: PANE, title: 'Limits settings', focus: true })
154
155const showLimits = ($: $, rateLimits: readonly SessionRateLimit[]) => $.ui.status(limitsLine(rateLimits))
156
157const report = async ($: $): Promise<string> => {
158  const current = familyOf(await $.session.model())
159  const usage = await $.session.usage()
160  const limits = readLimits(usage.rateLimits, await $.clock.now())
161  const list = FAMILIES.filter(f => watched.has(f))
162  const names = list.map(f => NAMES[f]).join(' and ')
163  const verdict =
164    list.length === 0
165      ? 'No models are watched, so deep research always runs.'
166      : allows(limits, rule)
167        ? `Deep research on ${names} is allowed right now.`
168        : `Deep research on ${names} asks first right now.`
169  const on = current ? ` You're on ${NAMES[current]}.` : ''
170
171  return [limitsLine(usage.rateLimits) ?? 'No usage reading yet.', verdict + on, ...explain(limits, rule)].join('\n')
172}
173
174export const register: Register = (on, options) => {
175  rule = {
176    resetWithinMinutes: Number(options.resetWithinMinutes ?? 30),
177    weeklyMaxPercent: Number(options.weeklyMaxPercent ?? 75),
178  }
179  watched = parseModels(String(options.models ?? 'fable'))
180  openOnStart = options.openOnStart !== false
181  base = JSON.stringify(snapshot())
182
183  on('session.start', async ($, e, next) => {
184    await $.command.register({
185      name: 'limits',
186      description: 'Show your 5h and 7d limits and whether deep research is allowed right now',
187    })
188    await loadSaved($)
189    const ran = await next(e)
190    showLimits($, (await $.session.usage()).rateLimits)
191    if (openOnStart) void openSettings($)
192
193    return ran
194  })
195
196  on('session.measure', async ($, e, next) => {
197    if (e.changed.includes('rateLimits')) showLimits($, e.rateLimits)
198
199    return next(e)
200  })
201
202  on('command.run', { command: 'limits' }, async ($, e) => {
203    if ((e.args ?? '').trim() === 'settings') {
204      await update($, notice, () => null)
205      await openSettings($)
206      return {}
207    }
208
209    await update($, notice, () => null)
210    await openSettings($)
211    return { text: await report($) }
212  })
213
214  // commands/settings.md lists it as /limits:settings; this answers it, so nothing reaches the model.
215  on('command.run', { command: 'limits:settings' }, async $ => {
216    await update($, notice, () => null)
217    await openSettings($)
218    return {}
219  })
220
221  // Draw /limits' answer as its own block, without the plugin-name prefix.
222  on('ui.render', { component: 'CommandOutput', props: { command: 'limits' } }, ($, e, next) => {
223    if (e.props.isErrored || !e.props.text) return next(e)
224    const { Box, Text } = $.ui.resolve(e)
225    // The row's text arrives as `limits: <what we answered>`; drop that prefix.
226    const [head, verdict, ...rest] = e.props.text.replace(/^limits:\s*/, '').split('\n')
227
228    return (
229      <Box flexDirection="column">
230        <Text bold>{head}</Text>
231        <Text>{verdict}</Text>
232        {rest.map((line, i) => (
233          <Text key={`line-${i}`}>{`  ${line}`}</Text>
234        ))}
235      </Box>
236    )
237  })
238
239  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
240    const table = $.ui.resolve(e)
241    const { Box, Button, Text } = table
242    const Select = 'Select' in table ? table.Select : undefined
243    const message = await read($, notice)
244    const limits = await currentLimits($)
245    const line = limitsLine((await $.session.usage()).rateLimits)
246    const verdict = allows(limits, rule) ? 'Deep research is allowed right now.' : 'Deep research will ask first right now.'
247
248    return (
249      <Box flexDirection="column" gap={1}>
250        <Box flexDirection="column">
251          <Text bold>Models to watch</Text>
252          <Text dimColor>Deep research on these asks first, unless both rules below pass.</Text>
253          <Box flexDirection="row" gap={1}>
254            {FAMILIES.map(f => (
255              <Button
256                key={`model-${f}`}
257                variant={watched.has(f) ? 'primary' : 'secondary'}
258                onPress={() => toggleModel($, f)}
259              >
260                {`${watched.has(f) ? '✓' : '○'} ${NAMES[f]}`}
261              </Button>
262            ))}
263          </Box>
264        </Box>
265        <Box flexDirection="column">
266          <Text bold>5h window resets within</Text>
267          {Select ? (
268            <Select
269              key="minutes"
270              value={String(rule.resetWithinMinutes)}
271              options={steps(MINUTE_STEPS, rule.resetWithinMinutes, ' min')}
272              onSelect={value => setMinutes($, value)}
273            />
274          ) : (
275            <Text>{rule.resetWithinMinutes} min</Text>
276          )}
277        </Box>
278        <Box flexDirection="column">
279          <Text bold>Weekly usage under</Text>
280          {Select ? (
281            <Select
282              key="percent"
283              value={String(rule.weeklyMaxPercent)}
284              options={steps(PERCENT_STEPS, rule.weeklyMaxPercent, '%')}
285              onSelect={value => setPercent($, value)}
286            />
287          ) : (
288            <Text>{rule.weeklyMaxPercent}%</Text>
289          )}
290        </Box>
291        <Box flexDirection="column">
292          <Text dimColor>{line ?? 'No usage reading yet.'}</Text>
293          <Text dimColor>{verdict}</Text>
294        </Box>
295        <Button key="open-on-start" dimColor onPress={() => toggleOpenOnStart($)}>
296          {`${openOnStart ? '✓' : '○'} Open this panel when a session starts`}
297        </Button>
298        {message && <Text color={message.startsWith("Couldn't") ? 'red' : 'green'}>{message}</Text>}
299      </Box>
300    )
301  })
302
303  on('prompt.submit', async ($, e, next) => {
304    // Our own re-send, or anything a plugin sent, goes straight through.
305    if (e.origin?.kind === 'plugin') return next(e)
306    const current = await watchedModel($)
307    if (!current || !(await isDeepResearch($, e.text))) return next(e)
308
309    const choice = await decide($, current, 'This looks like deep research')
310    if (choice === 'run') return next(e)
311    if (choice === 'cancel') return { drop: 'Deep research cancelled.' }
312    $.clock.after(0, () => void resend($, choice, e))
313
314    return { drop: `Switching to ${NAMES[familyOf(choice)!]} and sending your prompt again.` }
315  }).catch(($, e, next) => next(e))
316
317  // Slash commands like /deep-research never reach prompt.submit, so they get the same check here.
318  on('command.run', async ($, e, next) => {
319    if (e.origin?.kind === 'plugin' || !isResearchCommand(e.command)) return next(e)
320    const current = await watchedModel($)
321    if (!current) return next(e)
322
323    const choice = await decide($, current, `/${e.command} is about to run`)
324    if (choice === 'run') return next(e)
325    if (choice === 'cancel') return { text: 'Deep research cancelled.' }
326    $.clock.after(0, () => void rerun($, choice, e.command, e.args ?? ''))
327
328    return { text: `Switching to ${NAMES[familyOf(choice)!]} and running /${e.command} again.` }
329  }).catch(($, e, next) => next(e))
330}
331
hooks/rules.ts 120 lines
1import type { SessionRateLimit } from 'claude-code'
2
3const MINUTE = 60_000
4
5/** When a watched model is fine for deep research: both must hold. */
6export type Rule = { resetWithinMinutes: number; weeklyMaxPercent: number }
7
8/** What we know about the two windows right now; undefined when Claude Code has no reading. */
9export type Limits = { fiveHourPct?: number; resetInMinutes?: number; weeklyPct?: number }
10
11export type Family = 'fable' | 'opus' | 'sonnet' | 'haiku'
12
13export const NAMES: Record<Family, string> = { fable: 'Fable', opus: 'Opus', sonnet: 'Sonnet', haiku: 'Haiku' }
14
15export const familyOf = (model: string): Family | undefined => {
16  const name = model.toLowerCase()
17  if (name.includes('fable') || name.includes('mythos')) return 'fable'
18  if (name.includes('opus')) return 'opus'
19  if (name.includes('sonnet')) return 'sonnet'
20  if (name.includes('haiku')) return 'haiku'
21
22  return undefined
23}
24
25/** The models the check can watch: the expensive ones. */
26export const WATCHABLE: Family[] = ['fable', 'opus']
27
28/** The `models` setting, `fable, opus`, as the families it names. Anything else is ignored. */
29export const parseModels = (setting: string): Set<Family> =>
30  new Set(
31    setting
32      .split(/[\s,]+/)
33      .map(word => familyOf(word))
34      .filter((f): f is Family => f !== undefined && WATCHABLE.includes(f)),
35  )
36
37/** What the dialog offers on `current`: the other of Sonnet and Opus, keep, cancel. */
38export const choicesFor = (current: Family): string[] => [
39  ...(['sonnet', 'opus'] as const).filter(f => f !== current).map(f => NAMES[f]),
40  `Keep ${NAMES[current]}`,
41  'Cancel',
42]
43
44export const readLimits = (rateLimits: readonly SessionRateLimit[], now: number): Limits => {
45  const five = rateLimits.find(w => w.kind === 'five_hour')
46  const week = rateLimits.find(w => w.kind === 'seven_day')
47  const resetsAt = five?.resetsAt ? Date.parse(five.resetsAt) : undefined
48
49  return {
50    fiveHourPct: five?.percentUsed,
51    resetInMinutes: resetsAt === undefined ? undefined : Math.max(0, (resetsAt - now) / MINUTE),
52    weeklyPct: week?.percentUsed,
53  }
54}
55
56/** Fable is allowed only when the 5h window resets soon AND the week isn't too used. */
57export const allows = (limits: Limits, rule: Rule): boolean =>
58  limits.resetInMinutes !== undefined &&
59  limits.resetInMinutes < rule.resetWithinMinutes &&
60  limits.weeklyPct !== undefined &&
61  limits.weeklyPct < rule.weeklyMaxPercent
62
63/** Same format as the status line's reset countdown: 2d 4h, 3h 12m, 12m. */
64export const countdown = (minutes: number): string => {
65  const m = Math.max(0, Math.floor(minutes))
66  const d = Math.floor(m / 1440)
67  const h = Math.floor((m % 1440) / 60)
68  if (d > 0) return `${d}d ${h}h`
69  if (h > 0) return `${h}h ${m % 60}m`
70
71  return `${m}m`
72}
73
74/** The two conditions, each with whether it passes. */
75export const explain = (limits: Limits, rule: Rule): string[] => {
76  const mark = (ok: boolean) => (ok ? '✓' : '✗')
77  const reset =
78    limits.resetInMinutes === undefined
79      ? `? 5h reset not known yet (needs under ${rule.resetWithinMinutes} min)`
80      : `${mark(limits.resetInMinutes < rule.resetWithinMinutes)} 5h resets in ${countdown(limits.resetInMinutes)} (needs under ${rule.resetWithinMinutes} min)`
81  const week =
82    limits.weeklyPct === undefined
83      ? `? Weekly usage not known yet (needs under ${rule.weeklyMaxPercent}%)`
84      : `${mark(limits.weeklyPct < rule.weeklyMaxPercent)} Weekly ${Math.round(limits.weeklyPct)}% used (needs under ${rule.weeklyMaxPercent}%)`
85
86  return [reset, week]
87}
88
89const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
90
91const hhmm = (d: Date) => `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
92
93/** The clock face showing `d` to the nearest half hour: 21:20 is 🕤, 09:05 is 🕘. */
94export const clockFace = (d: Date): string => {
95  const halfHours = Math.round((d.getHours() * 60 + d.getMinutes()) / 30)
96  const hour = (Math.floor(halfHours / 2) + 11) % 12
97  const base = halfHours % 2 === 0 ? 0x1f550 : 0x1f55c
98
99  return String.fromCodePoint(base + hour)
100}
101
102/** `🕤 5h 1% 21:20 | 🗓️ 7d 50% Oct 6 21:20`, or undefined when there's no reading. */
103export const limitsLine = (rateLimits: readonly SessionRateLimit[]): string | undefined => {
104  const parts: string[] = []
105  const five = rateLimits.find(w => w.kind === 'five_hour')
106  const week = rateLimits.find(w => w.kind === 'seven_day')
107  if (five) {
108    const d = five.resetsAt ? new Date(five.resetsAt) : undefined
109    const at = d ? ` ${hhmm(d)}` : ''
110    parts.push(`${d ? clockFace(d) : '🕐'} 5h ${Math.round(five.percentUsed)}%${at}`)
111  }
112  if (week) {
113    const d = week.resetsAt ? new Date(week.resetsAt) : undefined
114    const at = d ? ` ${MONTHS[d.getMonth()]} ${d.getDate()} ${hhmm(d)}` : ''
115    parts.push(`🗓️ 7d ${Math.round(week.percentUsed)}%${at}`)
116  }
117
118  return parts.length > 0 ? parts.join(' | ') : undefined
119}
120
types/index.d.ts 9 lines
1/** The last thing the settings pane saved, or why it couldn't. */
2export type Notice = string | null
3
4declare module 'claude-code' {
5  interface PluginState {
6    limits: { notice: Notice }
7  }
8}
9