Resumes your Claude Code session automatically once the usage limit that stopped it resets

A Claude Code mod that picks your session back up once the usage limit that stopped it resets.
Hit the 5-hour limit in the middle of a task and walk away. no-limit reads when the limit resets, waits it out, and sends Claude a prompt to carry on where it left off. No tmux, no scripts, and it works the same on macOS, Linux and Windows.
You need Claude Code v2.1.287 or later (claude --version).
claude plugin marketplace add longmaba/no-limit
claude plugin install no-limit@longmaba-mods
Or from inside a session: /plugin install no-limit --marketplace longmaba/no-limit. Run /reload-plugins if a session was already open.
To try it once without installing, clone the repository and run claude --plugin-dir ./no-limit.
∙∙●∙∙ no-limit · 5h 15% while it's watching, with your 5-hour usage(-.-) zZ resets in 1h 12m while it waits for a reset\(^o^)/ back to work for a moment after it resumes/no-limit): usage bars for the 5-hour and weekly windows, the wait's progress, the resume prompt (editable), and the last few resumes| Command | What it does |
|---|---|
/no-limit | Open the dashboard |
/no-limit status | Print what's pending |
/no-limit now | Resume right away |
/no-limit cancel | Drop the pending resume |
/no-limit off / on | Stop or start resuming automatically |
/no-limit anim off / on | Keep the footer still, or animate it |
/no-limit message <text> | Set the prompt sent on resume (no text puts the default back) |
/no-limit test [minutes] | Pretend the limit was hit, to see it work (default 1 minute) |
Typing your own prompt while it waits cancels the pending resume: you're back, so your prompt wins.
rate_limit error, it reads the rate-limit windows Claude Code tracks for your account. It waits for the latest reset among the windows that are full, or the 5-hour window's when none reads full, plus one minute.claude -p run ends before any reset, so there's nothing to resume there.A mod is code that runs with your permissions. To see what this one hooks and calls before installing, clone it and run claude plugin validate ./no-limit. It reads your rate-limit figures, keeps its settings in Claude Code's plugin store, and submits one prompt per resume. It makes no network requests and touches no files.
claude plugin validate --strict .
claude plugin test .
Tested with Claude Code 2.1.289. Mods are early access, and the API can change between releases.
MIT
hooks/register.tsx 530 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { Limit, Pending, Resumed } from '../types'
5
6const pending = atom({ plugin: 'no-limit', key: 'pending' } as const, null)
7const lastResume = atom({ plugin: 'no-limit', key: 'lastResume' } as const, null)
8const history = atom({ plugin: 'no-limit', key: 'history' } as const, [])
9const limits = atom({ plugin: 'no-limit', key: 'limits' } as const, [])
10const enabled = atom({ plugin: 'no-limit', key: 'enabled' } as const, true)
11const message = atom({ plugin: 'no-limit', key: 'message' } as const, null)
12const animate = atom({ plugin: 'no-limit', key: 'animate' } as const, true)
13const frame = atom({ plugin: 'no-limit', key: 'frame' } as const, 0)
14const cheerUntil = atom({ plugin: 'no-limit', key: 'cheerUntil' } as const, 0)
15const hasGivenUp = atom({ plugin: 'no-limit', key: 'hasGivenUp' } as const, false)
16
17const PANE = 'no-limit'
18const MINUTE = 60_000
19// How often the countdown checks the clock: polling, not one long timer,
20// so a laptop that slept through the reset still resumes once it wakes
21const TICK_MS = 30_000
22const FRAME_MS = 400
23// Wait this long past the reported reset so the first request isn't refused
24const BUFFER_MS = 1 * MINUTE
25// A limit hit this soon after a resume counts as that resume failing
26const RETRY_WINDOW_MS = 10 * MINUTE
27const MAX_ATTEMPTS = 5
28// When the API reports no reset time: 15m, 30m, 60m, 60m, ...
29const FALLBACK_MS = 15 * MINUTE
30const FALLBACK_MAX_MS = 60 * MINUTE
31const HISTORY_SIZE = 10
32// Frames the footer cheers for after a resume, about five seconds
33const CHEER_FRAMES = 12
34
35const DEFAULT_MESSAGE =
36 'The usage limit has reset. Carry on with the task you were working on when the limit stopped you. If it was already finished, say so in one line.'
37
38const HELP = [
39 '/no-limit open the dashboard',
40 '/no-limit status print what is pending',
41 '/no-limit now resume right away',
42 '/no-limit cancel drop the pending resume',
43 '/no-limit off | on stop or start resuming automatically',
44 '/no-limit anim off | on still or animated footer indicator',
45 '/no-limit message … set the resume prompt (no text: back to the default)',
46 '/no-limit test [m] pretend the limit was hit, resuming in m minutes (default 1)',
47].join('\n')
48
49// The footer indicator's frames: a soft sweep while watching, a snooze
50// through the wait, a cheer on waking
51const SWEEP = ['●∙∙∙∙', '∙●∙∙∙', '∙∙●∙∙', '∙∙∙●∙', '∙∙∙∙●', '∙∙∙●∙', '∙∙●∙∙', '∙●∙∙∙']
52const SNOOZE = ['(-.-) ', '(-.-) z ', '(-.-) zZ ', '(-_-) zZz', '(-_-) Zz', '(-.-) z']
53const CHEER = ['\\(^o^)/', '\\(^_^)/', '\\(^o^)/', '(^_^)b ']
54
55let frames: Timer | undefined
56
57export const register: Register = on => {
58 on('session.start', async ($, e, next) => {
59 await $.command.register({
60 name: 'no-limit',
61 description: 'Auto-resume after a usage limit: open the dashboard, or status, now, cancel, off, on, anim, message, test',
62 argumentHint: '[status|now|cancel|off|on|anim on|anim off|message <text>|test [minutes]]',
63 })
64 // The engine draws a status line as a warning; this mod draws in the footer instead
65 $.ui.status(undefined)
66 await load($)
67 // A reload drops the old timers; the pending resume itself lives in $.state
68 $.clock.every(TICK_MS, () => void tick($))
69 await tick($)
70
71 return next(e)
72 })
73
74 on('session.measure', async ($, e, next) => {
75 if (e.changed.includes('rateLimits')) {
76 await update($, limits, () => e.rateLimits.map(w => ({ ...w })))
77 await refresh($)
78 }
79
80 return next(e)
81 })
82
83 on('classic.StopFailure', async ($, e, next) => {
84 if (e.error === 'rate_limit' && (await read($, enabled))) {
85 await arm($)
86 }
87
88 return next(e)
89 })
90
91 // The person typing at the prompt means they're back: their prompt wins
92 on('prompt.submit', async ($, e, next) => {
93 if (e.origin.kind === 'composer' && (await read($, pending)) !== null) {
94 await clear($)
95 }
96
97 return next(e)
98 })
99
100 on('command.run', { command: 'no-limit' }, async ($, e) => {
101 const [verb = '', ...rest] = e.args.trim().split(/\s+/)
102 const text = e.args.trim().slice(verb.length).trim()
103
104 switch (verb.toLowerCase()) {
105 case '':
106 case 'dashboard': {
107 const opened = await $.ui.open({ id: PANE, title: 'no-limit' })
108 return { text: opened.isPlaced ? 'Opened the no-limit dashboard.' : await describeStatus($) }
109 }
110 case 'status':
111 return { text: await describeStatus($) }
112 case 'now': {
113 const p = await read($, pending)
114 if (p === null) return { text: 'Nothing is pending.' }
115 // A prompt submitted from this hook would wait on the turn the hook holds
116 $.clock.after(0, () => void resumeNow($))
117 return { text: 'Resuming now.' }
118 }
119 case 'cancel':
120 await clear($)
121 return { text: 'Pending resume cancelled.' }
122 case 'off':
123 await setEnabled($, false)
124 return { text: 'Auto-resume is off. /no-limit on turns it back on.' }
125 case 'on':
126 await setEnabled($, true)
127 return { text: 'Auto-resume is on.' }
128 case 'anim':
129 case 'animation': {
130 const isOn = rest[0]?.toLowerCase() !== 'off'
131 await setAnimated($, isOn)
132 return { text: isOn ? 'Status line animation on.' : 'Status line animation off.' }
133 }
134 case 'message':
135 await setMessage($, text)
136 return { text: text === '' ? `Resume prompt reset to the default:\n${DEFAULT_MESSAGE}` : `Resume prompt set to:\n${text}` }
137 case 'test': {
138 const minutes = Number(rest[0] ?? 1)
139 if (!Number.isFinite(minutes) || minutes <= 0) return { text: 'Usage: /no-limit test [minutes]' }
140 await pretend($, minutes)
141 return { text: `Pretending the limit was hit. Resuming in ${until(minutes * MINUTE)}.` }
142 }
143 default:
144 return { text: HELP }
145 }
146 })
147
148 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
149 const p = await read($, pending)
150 if (p === null || e.props.hasSurvey) {
151 return next(e)
152 }
153
154 const now = await $.clock.now()
155 const retry = p.attempts > 0 ? ` · retry ${p.attempts}/${MAX_ATTEMPTS - 1}` : ''
156 const [done, left] = bar(progress(p, now), clamp(e.props.bodyColumns - 75, 6, 16))
157 const { Box, Button, Text } = $.ui.resolve(e)
158
159 return (
160 <Box flexDirection="row" gap={1}>
161 <Text color="yellow">(-.-) zZ</Text>
162 <Text bold>{label(p.window)}</Text>
163 <Box flexDirection="row">
164 <Text color="yellow">{done}</Text>
165 <Text dimColor>{left}</Text>
166 </Box>
167 <Text>
168 resuming in <Text bold>{until(p.resumeAt - now)}</Text>
169 <Text dimColor>{retry}</Text>
170 </Text>
171 <Button key="now" label="Resume now" variant="primary" onPress={() => resumeNow($)} />
172 <Button key="cancel" label="Cancel" onPress={() => clear($)} />
173 <Button key="details" label="Details" onPress={() => $.ui.open({ id: PANE, title: 'no-limit' })} />
174 </Box>
175 )
176 })
177
178 // The dim labels at the right of the prompt footer: the engine's own modes,
179 // then this mod's indicator, calm colors only
180 on('ui.render', { component: 'SessionMode' }, async ($, e) => {
181 const look = await indicator($)
182 const { Box, Text } = $.ui.resolve(e)
183
184 return (
185 <Box flexDirection="row">
186 {e.props.modes.length > 0 && <Text dimColor>{e.props.modes.join(' & ')} · </Text>}
187 {look.color === null ? <Text dimColor>{look.glyph}</Text> : <Text color={look.color}>{look.glyph}</Text>}
188 <Text dimColor> {look.text}</Text>
189 </Box>
190 )
191 })
192
193 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
194 const p = await read($, pending)
195 const windows = await read($, limits)
196 const isOn = await read($, enabled)
197 const prompt = await read($, message)
198 const resumes = await read($, history)
199 const now = await $.clock.now()
200 const width = clamp(e.props.bodyColumns - 36, 10, 40)
201 const { Box, Button, Text } = $.ui.resolve(e)
202 const Input = e.surface === 'mobile' ? null : $.ui.resolve(e).Input
203
204 const badge =
205 p !== null
206 ? { text: '(-.-) zZ WAITING FOR RESET', color: 'yellow' }
207 : isOn
208 ? { text: '(o.o) WATCHING', color: 'green' }
209 : { text: '(-_-) OFF', color: 'gray' }
210 const shownWindows = windows.filter(w => w.kind === 'five_hour' || w.kind === 'seven_day')
211
212 return (
213 <Box flexDirection="column" gap={1} paddingX={1}>
214 <Box flexDirection="row" justifyContent="space-between">
215 <Text bold color={badge.color}>
216 {badge.text}
217 </Text>
218 <Text dimColor>auto-resume {isOn ? 'on' : 'off'}</Text>
219 </Box>
220
221 <Box flexDirection="column">
222 <Text bold>Usage</Text>
223 {shownWindows.length === 0 && <Text dimColor>No readings yet: they arrive with the next reply.</Text>}
224 {shownWindows.map(w => {
225 const [used, free] = bar(w.percentUsed / 100, width)
226 const resetsIn = w.resetsAt === undefined ? '' : `resets in ${until(Date.parse(w.resetsAt) - now)}`
227 return (
228 <Box key={`limit-${w.kind}`} flexDirection="row" gap={1}>
229 <Text>{(w.kind === 'five_hour' ? '5-hour' : 'Weekly').padEnd(6)}</Text>
230 <Box flexDirection="row">
231 <Text color={heat(w.percentUsed)}>{used}</Text>
232 <Text dimColor>{free}</Text>
233 </Box>
234 <Text bold color={heat(w.percentUsed)}>
235 {`${Math.round(w.percentUsed)}%`.padStart(4)}
236 </Text>
237 <Text dimColor>{resetsIn}</Text>
238 </Box>
239 )
240 })}
241 </Box>
242
243 {p !== null && (
244 <Box flexDirection="column" borderStyle="round" borderColor="yellow" paddingX={1}>
245 <Text color="yellow" bold>
246 {label(p.window)} hit. Napping until it resets.
247 </Text>
248 <Box flexDirection="row" gap={1}>
249 <Box flexDirection="row">
250 <Text color="yellow">{bar(progress(p, now), width)[0]}</Text>
251 <Text dimColor>{bar(progress(p, now), width)[1]}</Text>
252 </Box>
253 <Text>
254 resuming in <Text bold>{until(p.resumeAt - now)}</Text>
255 </Text>
256 </Box>
257 {p.attempts > 0 && (
258 <Text dimColor>
259 Retry {p.attempts} of {MAX_ATTEMPTS - 1}: the last resume hit the limit again.
260 </Text>
261 )}
262 </Box>
263 )}
264
265 <Box flexDirection="column">
266 <Text bold>Resume prompt{prompt === null ? ' (default)' : ''}</Text>
267 <Text dimColor wrap="wrap">
268 {prompt ?? DEFAULT_MESSAGE}
269 </Text>
270 {Input !== null && (
271 <Input
272 key="message"
273 placeholder="Type a new resume prompt, Enter to save (empty: default)"
274 submitLabel="Save"
275 onSubmit={value => setMessage($, value.trim())}
276 />
277 )}
278 </Box>
279
280 <Box flexDirection="column">
281 <Text bold>History</Text>
282 {resumes.length === 0 && <Text dimColor>No resumes yet.</Text>}
283 {resumes
284 .slice()
285 .reverse()
286 .slice(0, 5)
287 .map(r => (
288 <Text>
289 <Text color="green">✓</Text> {ago(now - r.at).padEnd(8)} after the {label(r.window).toLowerCase()}
290 <Text dimColor>, waited {until(r.waitedMs)}</Text>
291 </Text>
292 ))}
293 </Box>
294
295 <Box flexDirection="row" gap={1} flexWrap="wrap">
296 {p !== null && <Button key="now" label="Resume now" hotkey="r" variant="primary" onPress={() => resumeNow($)} />}
297 {p !== null && <Button key="cancel" label="Cancel" hotkey="c" onPress={() => clear($)} />}
298 <Button key="toggle" label={isOn ? 'Turn off' : 'Turn on'} hotkey="o" onPress={() => setEnabled($, !isOn)} />
299 <Button key="test" label="Test (1 min)" hotkey="t" onPress={() => pretend($, 1)} />
300 </Box>
301 </Box>
302 )
303 })
304}
305
306// Seed the session's values from what earlier sessions kept
307async function load($: EngineInterface) {
308 const kept = {
309 enabled: await $.store.get('enabled'),
310 message: await $.store.get('message'),
311 animate: await $.store.get('animate'),
312 history: await $.store.get('history'),
313 }
314 await update($, enabled, () => kept.enabled !== false)
315 await update($, animate, () => kept.animate !== false)
316 await update($, message, () => (typeof kept.message === 'string' && kept.message !== '' ? kept.message : null))
317 await update($, history, list => (list.length === 0 && Array.isArray(kept.history) ? (kept.history as Resumed[]) : list))
318
319 const { rateLimits } = await $.session.usage()
320 if (rateLimits.length > 0) {
321 await update($, limits, () => rateLimits.map(w => ({ ...w })))
322 }
323}
324
325async function arm($: EngineInterface) {
326 const now = await $.clock.now()
327 const last = await read($, lastResume)
328 const attempts = last !== null && now - last.at < RETRY_WINDOW_MS ? last.attempts + 1 : 0
329
330 if (attempts >= MAX_ATTEMPTS) {
331 await clear($)
332 await update($, hasGivenUp, () => true)
333 await refresh($)
334 $.ui.toast(`no-limit: still limited after ${MAX_ATTEMPTS - 1} resumes, giving up. /no-limit test 1 to try again.`)
335 return
336 }
337
338 const reset = await findReset($, now)
339 const p: Pending =
340 reset !== null
341 ? { resumeAt: reset.at + BUFFER_MS, armedAt: now, window: reset.window, attempts }
342 : { resumeAt: now + Math.min(FALLBACK_MS * 2 ** attempts, FALLBACK_MAX_MS), armedAt: now, window: 'unknown', attempts }
343
344 await update($, hasGivenUp, () => false)
345 await update($, pending, () => p)
346 $.ui.toast(`${label(p.window)} hit. Resuming automatically in ${until(p.resumeAt - now)}.`)
347 await tick($)
348}
349
350async function pretend($: EngineInterface, minutes: number) {
351 const now = await $.clock.now()
352 await update($, hasGivenUp, () => false)
353 await refresh($)
354 await update($, pending, () => ({ resumeAt: now + minutes * MINUTE, armedAt: now, window: 'test', attempts: 0 }))
355 await tick($)
356}
357
358// The latest future reset among the windows that are full; failing that the
359// 5-hour window's, since that's the one a busy session usually runs out of
360async function findReset($: EngineInterface, now: number) {
361 const { rateLimits } = await $.session.usage()
362 const future = rateLimits
363 .map(w => ({ window: w.kind, at: w.resetsAt === undefined ? NaN : Date.parse(w.resetsAt), percentUsed: w.percentUsed }))
364 .filter(w => Number.isFinite(w.at) && w.at > now)
365 const full = future.filter(w => w.percentUsed >= 100)
366 const candidates = full.length > 0 ? full : future.filter(w => w.window === 'five_hour')
367
368 return candidates.reduce<{ window: string; at: number } | null>(
369 (latest, w) => (latest === null || w.at > latest.at ? { window: w.window, at: w.at } : latest),
370 null,
371 )
372}
373
374async function tick($: EngineInterface) {
375 const p = await read($, pending)
376 if (p !== null && (await $.clock.now()) >= p.resumeAt) {
377 await resume($, p)
378 return
379 }
380
381 await refresh($)
382 $.ui.invalidate('ui.render')
383}
384
385async function resume($: EngineInterface, p: Pending) {
386 const now = await $.clock.now()
387 const entry: Resumed = { at: now, window: p.window, waitedMs: now - p.armedAt }
388 await update($, pending, () => null)
389 await update($, lastResume, () => ({ at: now, attempts: p.attempts }))
390 await update($, history, list => [...list, entry].slice(-HISTORY_SIZE))
391 await $.store.set('history', await read($, history))
392 if (await read($, animate)) {
393 const at = await read($, frame)
394 await update($, cheerUntil, () => at + CHEER_FRAMES)
395 }
396 await refresh($)
397 $.ui.toast('Usage limit reset: resuming the session')
398
399 const text = (await read($, message)) ?? DEFAULT_MESSAGE
400 void $.prompt.submit({ text })
401}
402
403async function resumeNow($: EngineInterface) {
404 const p = await read($, pending)
405 if (p !== null) {
406 await resume($, p)
407 }
408}
409
410async function clear($: EngineInterface) {
411 await update($, pending, () => null)
412 await refresh($)
413}
414
415async function setEnabled($: EngineInterface, isOn: boolean) {
416 await $.store.set('enabled', isOn)
417 await update($, enabled, () => isOn)
418 if (!isOn) {
419 await update($, pending, () => null)
420 }
421 await refresh($)
422}
423
424async function setAnimated($: EngineInterface, isOn: boolean) {
425 await $.store.set('animate', isOn)
426 await update($, animate, () => isOn)
427 await refresh($)
428}
429
430async function setMessage($: EngineInterface, text: string) {
431 if (text === '') {
432 await $.store.delete('message')
433 await update($, message, () => null)
434 } else {
435 await $.store.set('message', text)
436 await update($, message, () => text)
437 }
438}
439
440// Start or stop the frame timer; the footer reads the state itself, so a
441// still indicator (animation off, auto-resume off, given up) needs no timer
442async function refresh($: EngineInterface) {
443 const isMoving = (await read($, animate)) && (await read($, enabled)) && !(await read($, hasGivenUp))
444 if (isMoving && frames === undefined) {
445 frames = $.clock.every(FRAME_MS, () => void update($, frame, n => n + 1))
446 } else if (!isMoving && frames !== undefined) {
447 frames.cancel()
448 frames = undefined
449 }
450}
451
452async function indicator($: EngineInterface): Promise<{ glyph: string; color: string | null; text: string }> {
453 const n = (await read($, animate)) ? await read($, frame) : 0
454 const p = await read($, pending)
455
456 if (n < (await read($, cheerUntil))) {
457 return { glyph: CHEER[n % CHEER.length] ?? '', color: 'green', text: 'back to work' }
458 }
459 if (!(await read($, enabled))) {
460 return { glyph: '○', color: null, text: 'no-limit off' }
461 }
462 if (await read($, hasGivenUp)) {
463 return { glyph: '(-_-)', color: null, text: 'no-limit stopped retrying · /no-limit test' }
464 }
465 if (p !== null) {
466 const now = await $.clock.now()
467 return { glyph: SNOOZE[n % SNOOZE.length] ?? '', color: 'cyan', text: `resets in ${until(p.resumeAt - now)}` }
468 }
469
470 const five = (await read($, limits)).find(w => w.kind === 'five_hour')
471 const usage = five === undefined ? '' : ` · 5h ${Math.round(five.percentUsed)}%`
472 return { glyph: SWEEP[n % SWEEP.length] ?? '', color: 'green', text: `no-limit${usage}` }
473}
474
475async function describeStatus($: EngineInterface) {
476 const isOn = await read($, enabled)
477 const p = await read($, pending)
478 const head = isOn ? 'Auto-resume is on.' : 'Auto-resume is off.'
479 if (p === null) return `${head} Nothing is pending.\n\n${HELP}`
480
481 const now = await $.clock.now()
482 return `${head} ${label(p.window)} hit; resuming in ${until(p.resumeAt - now)}.\n\n${HELP}`
483}
484
485function label(window: string) {
486 switch (window) {
487 case 'five_hour':
488 return '5-hour limit'
489 case 'seven_day':
490 return 'Weekly limit'
491 case 'test':
492 return 'Test limit'
493 default:
494 return 'Usage limit'
495 }
496}
497
498function progress(p: Pending, now: number) {
499 const span = p.resumeAt - p.armedAt
500 return span <= 0 ? 1 : (now - p.armedAt) / span
501}
502
503// A bar's filled and empty halves, drawn in two colors by the caller
504function bar(fraction: number, width: number): [string, string] {
505 const filled = Math.round(clamp(fraction, 0, 1) * width)
506 return ['█'.repeat(filled), '░'.repeat(width - filled)]
507}
508
509function heat(percent: number) {
510 return percent >= 90 ? 'red' : percent >= 70 ? 'yellow' : 'green'
511}
512
513function clamp(n: number, low: number, high: number) {
514 return Math.min(high, Math.max(low, n))
515}
516
517function until(ms: number) {
518 const minutes = Math.max(0, Math.ceil(ms / MINUTE))
519 const d = Math.floor(minutes / 1440)
520 const h = Math.floor((minutes % 1440) / 60)
521 const m = minutes % 60
522
523 return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`
524}
525
526function ago(ms: number) {
527 const minutes = Math.floor(ms / MINUTE)
528 return minutes < 1 ? 'just now' : `${until(ms)} ago`
529}
530types/index.d.ts 40 lines1/** A resume waiting for the usage limit to reset. */
2export type Pending = {
3 /** When to resume, in `$.clock.now()` milliseconds. */
4 resumeAt: number
5 /** When the limit was hit, so the wait can be drawn as progress. */
6 armedAt: number
7 /** Which window stopped the session (`five_hour`, `seven_day`), or `unknown`. */
8 window: string
9 /** How many resumes in a row have hit the limit again. */
10 attempts: number
11}
12
13/** The last resume this mod sent, so a limit hit right after it counts as a retry. */
14export type LastResume = { at: number; attempts: number }
15
16/** One resume, for the dashboard's history. */
17export type Resumed = { at: number; window: string; waitedMs: number }
18
19/** One rate-limit window as the last API response reported it. */
20export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
21
22declare module 'claude-code' {
23 interface PluginState {
24 'no-limit': {
25 pending: Pending | null
26 lastResume: LastResume | null
27 history: Resumed[]
28 limits: Limit[]
29 enabled: boolean
30 message: string | null
31 animate: boolean
32 /** The footer indicator's animation frame, counted up while it moves. */
33 frame: number
34 /** The frame the after-resume cheer lasts until. */
35 cheerUntil: number
36 hasGivenUp: boolean
37 }
38 }
39}
40