SLOPSHOPPER

usage-band

Shows the current session and weekly usage limits in a band above the prompt of the Claude desktop app, and locks a working session against Escape and stray…

newpanebandcommandtoastprompt
v0.3.7MITupdated 2026-10-22erdembircan/claude-usage-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-band
› 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 › /usage-band ⎿ usage-band: Usage band hidden. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

usage-band

A Claude Code mod that shows your plan's usage limits in a band above the prompt of the Claude desktop app, and can lock a session so that a stray Escape or prompt does not interrupt it. Each limit is one small mark and a percentage:

(hourglass) (dial) 42%    (calendar) (dial) 92%    (padlock)
  • The hourglass is the current session, the five-hour window. The calendar is the weekly one, seven days.
  • The dial's outer ring is the share of the limit used. The wedge in its centre is the time left before the window resets: full when the window opens, empty at the reset.
  • The ring and the percentage are blue below 70%, amber from 70%, and red from 90%.
  • The padlock is the session lock, and its button: open and grey while the session is free, shut and blue while it is locked. Press it to lock the session, and again to unlock it.
  • The band draws on the desktop surface only, and only once a response has reported a limit. An account with no subscription limits (an API key) never sees it.
  • A window past its reset time stays on the band at 0% with an empty wedge, until the next reply reports the new window.
  • /usage-band hides the band for the session, or shows it again.

The marks are drawn as images, which cannot read the app's theme, so their colours are mid-tones chosen to hold on both the light and the dark ground.

Session lock

Lock a session you want left alone while it works: press the padlock in the band, or run /usage-band lock. Until you unlock it:

  • Escape does not stop it. A small pane opens beside the transcript and takes the keyboard. Escape pressed there is the pane's own close key, and the mod refuses that close, so the key goes no further: the turn and its background agents keep running. Click somewhere else and the pane takes the keyboard back.
  • The prompt box is closed. It takes no typing while the session is locked, and a prompt you type somewhere else, at a terminal, over Remote Control or from Slack, is refused. A draft that was already in the box when you locked is set aside and put back when you unlock; nothing typed during the lock comes back. The rest of the session carries on: what you send with a click, such as an artifact comment sent to Claude or an answer from the next-step box, still goes in, and so do background agents and tasks reporting back, scheduled wake-ups, other sessions, and the desktop app's own events, such as a failed check on a pull request it watches.

Unlock with Unlock in the pane, or with the padlock in the band. /usage-band unlock does the same from wherever you can still type it, such as over Remote Control. The Stop button is left alone: a click on it stops the turn, locked or not.

The limits of it:

  • A mod cannot veto an interrupt. The lock works one step earlier, by keeping Escape from reaching the prompt box, so it holds only while the lock pane has the keyboard. Where the app will not hand it over (a dialog is open, or the cursor is in another text field with something typed), Escape does what it always did until the pane has the keyboard again. A locked session asks for it four times a second.
  • This rests on how the desktop app handles Escape today, in October 2026. It is not something the mod API promises, and an update of the app can change it.
  • The desktop app sends what is typed in its box and what it sends on its own the same way, so the lock cannot tell them apart when they arrive. It closes the box instead, and lets everything the app sends through.
  • While a turn runs, lock with the padlock: a typed /usage-band lock may wait for the turn to end.
  • With the keyboard held by the pane, a permission prompt or a question is answered with the mouse.
  • The lock is per session and needs the desktop app. In a terminal /usage-band lock says so and locks nothing.
  • The padlock is a picture with a blank button laid over it, since a mod's button can show only text. A screen reader reads the picture as Session lock: on or off, then a button with no name. /usage-band lock and Unlock in the pane work without it.

Install

At the prompt of a Claude Code session in a terminal:

/plugin install usage-band --marketplace erdembircan/plugin-marketplace

Answer y to add the marketplace, then pick a scope. Installed at the user scope, the mod loads in every session after, the desktop app's included.

From a shell:

claude plugin install usage-band --marketplace erdembircan/plugin-marketplace

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with erdembircan/plugin-marketplace, then Add plugin.

A newer version comes with claude plugin update usage-band@erdembircan-plugins and a new session.

Develop

claude --plugin-dir .

loads the folder for one session and reloads the module on save.

claude plugin validate .
claude plugin test .

A load with --plugin-dir also writes the API types of the build you run to .claude-plugin/types, which git ignores. tsconfig.json extends the one laid there, so after a first load:

tsc -p .

A change reaches installs once it is on master with version bumped in .claude-plugin/plugin.json.

Diagrams

docs/usage-band-feed.bpmn is a BPMN 2.0 file: what keeps the band's figures current, and what each redraw of the band changes. docs/bpmn.html renders it with bpmn-js; any BPMN tool opens the file itself.

Both are written by docs/make_bpmn.py. After changing it, regenerate them:

python3 docs/make_bpmn.py docs

License

MIT, generated and verified with license-wizard (license-wizard --verify --strict).

Source 2 files
hooks/register.tsx 552 lines
1import { atom, read, update } from 'claude-code'
2import type {
3  EngineInterface,
4  PromptOrigin,
5  Register,
6  RenderElement,
7  Timer,
8} from 'claude-code'
9
10import type { Limit } from '../types'
11
12const limits = atom({ plugin: 'usage-band', key: 'limits' } as const, [])
13const isHidden = atom({ plugin: 'usage-band', key: 'isHidden' } as const, false)
14const isLocked = atom({ plugin: 'usage-band', key: 'isLocked' } as const, false)
15const draft = atom({ plugin: 'usage-band', key: 'draft' } as const, '')
16
17const MINUTE = 60_000
18
19// One mark per window: an icon for the kind of window, then a dial. The dial's
20// outer ring is the share of the limit used, the wedge in its centre the share
21// of the window's time still left.
22const MARK_WIDTH = 41
23const MARK_HEIGHT = 22
24const RING = 59.06 // the circumference of the outer ring, r = 9.4
25const WEDGE = 18.85 // the circumference the wedge is drawn on, r = 3
26// The Svg is drawn as an image, so it cannot read the app's theme: these are
27// mid-tones that hold on both the light and the dark ground. The percentage
28// takes the same colour as its ring.
29const LOW = '#3A82DA'
30const WARNING = '#C98414'
31const CRITICAL = '#D2544A'
32const QUIET = '#87867F'
33const TRACK = '#8C8C8C'
34
35const HOURGLASS =
36  '<path d="M3.4 1.6h6.2M3.4 11.4h6.2M4 1.6v1.6l2.5 3.3L9 3.2V1.6M4 11.4V9.8l2.5-3.3L9 9.8v1.6"/>'
37const CALENDAR =
38  '<rect x="1.8" y="2.8" width="9.4" height="8.4" rx="1.6"/><path d="M1.8 5.7h9.4M4.4 1.5v2.4M8.6 1.5v2.4"/>'
39
40// The padlock is drawn like the window icons, on its own: open and quiet while
41// the session is free, shut and in the band's blue while it is locked.
42const LOCK_WIDTH = 13
43const PADLOCK_BODY = '<rect x="2.6" y="5.9" width="7.8" height="5.5" rx="1.4"/>'
44const SHACKLE_OPEN = 'M4.3 5.9V3.3a2.2 2.2 0 0 1 4.4 0'
45const SHACKLE_SHUT = 'M4.3 5.9V4.2a2.2 2.2 0 0 1 4.4 0v1.7'
46const KEYHOLE = 'M6.5 8.2v.9'
47// In the band the padlock is the lock's whole control. A Button's face is text
48// and an Svg takes no press, so a Button with a blank label is laid over the
49// padlock and takes the press for it. The label is an en space: it draws
50// nothing, and keeps the Button about as wide as it is tall.
51const BLANK = '\u2002'
52// How far the Box that holds the Button reaches past the padlock on each side,
53// in columns. The desktop draws a Button no wider than the Box it is in, and
54// this Button is wider than the padlock.
55const REACH = 2
56
57const SESSION = {
58  kind: 'five_hour',
59  label: 'Current session',
60  icon: HOURGLASS,
61  span: 5 * 60 * MINUTE,
62}
63const WEEK = {
64  kind: 'seven_day',
65  label: 'Weekly',
66  icon: CALENDAR,
67  span: 7 * 24 * 60 * MINUTE,
68}
69
70// The lock is one pane. `focus` asks the surface for the keyboard. Under
71// `closeOnEscape` Escape is the person's close of the pane, which the ui.close
72// hook refuses while locked: the key ends there and never stops the turn. Left
73// out, Escape would hand the keyboard back to the prompt box instead.
74const PANE = 'usage-band-lock'
75const OPEN = {
76  id: PANE,
77  title: 'Session lock',
78  focus: true,
79  closeOnEscape: true,
80} as const
81// How often a locked session checks that the pane still has the keyboard.
82const WATCH = 250
83
84// A prompt the person typed somewhere the lock cannot close: Enter at a
85// terminal, a message over Remote Control, a ping from Slack. The desktop app
86// is not on the list. It submits what is typed in its box as `sdk`, but it
87// sends the same way what goes out on a click or of its own accord, such as an
88// artifact comment sent to Claude, so `sdk` cannot tell typing apart. It need
89// not: the box takes no typing while locked (see prompt.edit), so nothing typed
90// there can reach the gate. Every other prompt passes a locked session: a
91// background task or agent reporting back, a schedule, another session, a
92// plugin, and whatever the desktop app sends.
93const PERSONS: readonly PromptOrigin['kind'][] = [
94  'composer',
95  'bridge',
96  'slack-ping',
97]
98
99const LOCK_ON = 'Session lock: on'
100const LOCK_OFF = 'Session lock: off'
101const LOCKED = 'Session locked.'
102const UNLOCKED = 'Session unlocked.'
103const NEEDS_DESKTOP = 'The session lock needs the Claude desktop app.'
104const USAGE = 'Usage: /usage-band, /usage-band lock or /usage-band unlock.'
105const REFUSAL =
106  'The session is locked, so this prompt was not sent. Unlock it first: press Unlock, or run /usage-band unlock.'
107const STAYS = 'the session is locked'
108const NOT_RETURNED =
109  'usage-band: your draft could not be put back. Run /usage-band unlock to try again.'
110
111const windowOf = (list: readonly Limit[], kind: string) =>
112  list.find(one => one.kind === kind) ??
113  list.find(one => one.kind.startsWith(kind))
114
115// Past its reset time a window is empty again: it is shown at 0% until the next
116// reply reports the new one.
117const hasReset = (limit: Limit, now: number) =>
118  limit.resetsAt !== undefined && Date.parse(limit.resetsAt) <= now
119
120const remaining = (resetsAt: string | undefined, now: number) => {
121  const left = resetsAt === undefined ? NaN : Date.parse(resetsAt) - now
122
123  if (Number.isNaN(left)) {
124    return ''
125  }
126
127  const minutes = Math.ceil(left / MINUTE)
128  const days = Math.floor(minutes / 1440)
129  const hours = Math.floor((minutes % 1440) / 60)
130
131  if (days > 0) {
132    return `resets in ${days}d ${hours}h`
133  }
134
135  if (hours > 0) {
136    return `resets in ${hours}h ${minutes % 60}m`
137  }
138
139  return `resets in ${minutes}m`
140}
141
142const share = (value: number) =>
143  Number.isNaN(value) ? 0 : Math.min(1, Math.max(0, value))
144
145const toneOf = (percent: number) =>
146  percent >= 90 ? CRITICAL : percent >= 70 ? WARNING : LOW
147
148const mark = (icon: string, used: number, left: number, colour: string) =>
149  [
150    `<svg xmlns="http://www.w3.org/2000/svg" width="${MARK_WIDTH}" height="${MARK_HEIGHT}" viewBox="0 0 ${MARK_WIDTH} ${MARK_HEIGHT}">`,
151    `<g transform="translate(0 4.5)" fill="none" stroke="${QUIET}" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">${icon}</g>`,
152    `<circle cx="30" cy="11" r="9.4" fill="none" stroke="${TRACK}" stroke-opacity="0.3" stroke-width="2.6"/>`,
153    `<circle cx="30" cy="11" r="9.4" fill="none" stroke="${colour}" stroke-width="2.6" stroke-dasharray="${(RING * used).toFixed(2)} ${RING}" transform="rotate(-90 30 11)"/>`,
154    `<circle cx="30" cy="11" r="3" fill="none" stroke="${TRACK}" stroke-opacity="0.3" stroke-width="6"/>`,
155    `<circle cx="30" cy="11" r="3" fill="none" stroke="${QUIET}" stroke-width="6" stroke-dasharray="${(WEDGE * left).toFixed(2)} ${WEDGE}" transform="rotate(-90 30 11)"/>`,
156    '</svg>',
157  ].join('')
158
159const padlock = (isShut: boolean) =>
160  [
161    `<svg xmlns="http://www.w3.org/2000/svg" width="${LOCK_WIDTH}" height="${MARK_HEIGHT}" viewBox="0 0 ${LOCK_WIDTH} ${MARK_HEIGHT}">`,
162    `<g transform="translate(0 4.5)" fill="none" stroke="${isShut ? LOW : QUIET}" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">`,
163    PADLOCK_BODY,
164    `<path d="${isShut ? SHACKLE_SHUT : SHACKLE_OPEN}${KEYHOLE}"/>`,
165    '</g>',
166    '</svg>',
167  ].join('')
168
169// The engine's own band is an empty Box; another mod's band has content.
170const hasContent = (drawn: RenderElement) =>
171  drawn.type !== 'Box' || (drawn.children?.length ?? 0) > 0
172
173const sameLimits = (a: readonly Limit[], b: readonly Limit[]) =>
174  a.length === b.length &&
175  a.every((one, i) => {
176    const other = b[i]
177
178    return (
179      other !== undefined &&
180      one.kind === other.kind &&
181      one.percentUsed === other.percentUsed &&
182      one.resetsAt === other.resetsAt
183    )
184  })
185
186// Claude Code stops reporting a window once its reset time has passed, so a
187// reading replaces the stored window of its kind and never removes one. A write
188// redraws the band, so only a list that differs is written.
189const keep = async ($: EngineInterface, reported: readonly Limit[]) => {
190  const stored = await read($, limits)
191  const next = [
192    ...stored.map(old => reported.find(one => one.kind === old.kind) ?? old),
193    ...reported.filter(one => !stored.some(old => old.kind === one.kind)),
194  ]
195
196  if (!sameLimits(stored, next)) {
197    await update($, limits, () => next)
198  }
199}
200
201const refresh = async ($: EngineInterface) => {
202  const { rateLimits } = await $.session.usage()
203  await keep($, rateLimits)
204}
205
206const isPersons = (origin: PromptOrigin) => PERSONS.includes(origin.kind)
207
208// Empties the prompt box and answers what was in it. Over text there the
209// surface refuses the pane the keyboard, and Escape in the box stops the turn.
210const emptied = async ($: EngineInterface) => {
211  const { text } = await $.prompt.read()
212
213  if (text.trim() === '') {
214    return ''
215  }
216
217  const { isFilled } = await $.prompt.fill({ text: '', mode: 'replace' })
218
219  return isFilled ? text : ''
220}
221
222// The timer and the two flags live as long as the module: a reload starts them
223// over, and the session's start puts the lock back.
224let watch: Timer | undefined
225let isHolding = false
226let hadKeyboard = false
227
228// Gives the pane the keyboard while the session is locked: opens it when it is
229// gone, and asks for the keyboard again when the person has clicked away.
230const hold = async ($: EngineInterface) => {
231  if (isHolding || !(await read($, isLocked))) {
232    return
233  }
234
235  isHolding = true
236
237  try {
238    const pane = (await $.ui.panes()).find(one => one.id === PANE)
239
240    if (pane?.isFocused) {
241      return
242    }
243
244    // What has landed in the box while locked was not meant for it: it goes.
245    await emptied($)
246    await $.ui.open(OPEN)
247  } finally {
248    isHolding = false
249  }
250}
251
252const disarm = () => {
253  watch?.cancel()
254  watch = undefined
255}
256
257const arm = ($: EngineInterface) => {
258  disarm()
259  watch = $.clock.every(WATCH, () => void hold($))
260}
261
262const engage = async ($: EngineInterface) => {
263  if (!(await read($, isLocked))) {
264    // The draft in the box as the lock goes on is the person's: it is set
265    // aside, and comes back on unlock.
266    const text = await emptied($)
267
268    if (text !== '') {
269      await update($, draft, () => text)
270    }
271
272    await update($, isLocked, () => true)
273  }
274
275  arm($)
276  await hold($)
277}
278
279const release = async ($: EngineInterface) => {
280  await update($, isLocked, () => false)
281  disarm()
282  await $.ui.close({ id: PANE })
283
284  const text = await read($, draft)
285
286  if (text === '') {
287    return
288  }
289
290  const { isFilled } = await $.prompt.fill({ text, mode: 'append' })
291
292  if (isFilled) {
293    await update($, draft, () => '')
294  } else {
295    $.ui.toast(NOT_RETURNED)
296  }
297}
298
299// A reload keeps $.state and drops the timer; /clear, /resume and /branch
300// reset $.state and may leave the pane up.
301const settle = async ($: EngineInterface) => {
302  if (await read($, isLocked)) {
303    arm($)
304    await hold($)
305
306    return
307  }
308
309  disarm()
310
311  if ((await $.ui.panes()).some(one => one.id === PANE)) {
312    await $.ui.close({ id: PANE })
313  }
314}
315
316export const register: Register = on => {
317  on('session.start', async ($, e, next) => {
318    await $.command.register({
319      name: 'usage-band',
320      description: 'Show or hide the usage band, or lock and unlock the session',
321      argumentHint: '[lock|unlock]',
322      // A session is locked, and unlocked, while it works.
323      immediate: true,
324    })
325    await refresh($)
326    $.clock.every(MINUTE, () => $.ui.invalidate('ui.render'))
327    await settle($)
328
329    return next(e)
330  })
331
332  // /clear, /resume and /branch reset $.state without a session.start.
333  on(
334    'classic.SessionStart',
335    { source: ['clear', 'resume', 'fork'] },
336    async ($, e, next) => {
337      await refresh($)
338      await settle($)
339
340      return next(e)
341    },
342  )
343
344  on('session.measure', async ($, e, next) => {
345    // Every measurement carries the exact figures, whatever `changed` names.
346    await keep($, e.rateLimits)
347
348    return next(e)
349  })
350
351  on('command.run', { command: 'usage-band' }, async ($, e) => {
352    const wanted = e.args.trim()
353
354    if (wanted === 'lock') {
355      if (!(await $.session.surfaces()).includes('desktop')) {
356        return { text: NEEDS_DESKTOP }
357      }
358
359      await engage($)
360
361      return { text: LOCKED }
362    }
363
364    if (wanted === 'unlock') {
365      await release($)
366
367      return { text: UNLOCKED }
368    }
369
370    if (wanted !== '') {
371      return { text: USAGE }
372    }
373
374    const hidden = await update($, isHidden, value => !value)
375
376    return { text: hidden ? 'Usage band hidden.' : 'Usage band shown.' }
377  })
378
379  // The prompt box takes no typing while locked: every edit is answered with an
380  // empty box, and the pane asks for the keyboard back.
381  on('prompt.edit', async ($, e, next) => {
382    if (!(await read($, isLocked))) {
383      return next(e)
384    }
385
386    void hold($)
387
388    return { text: '', cursor: 0 }
389  }).catch((_, e, next) => next(e))
390
391  // A prompt typed somewhere other than the desktop's box is refused while
392  // locked, and not kept. A dead gate must never swallow a prompt: it fails open.
393  on('prompt.submit', async ($, e, next) => {
394    if (!(await read($, isLocked)) || !isPersons(e.origin)) {
395      return next(e)
396    }
397
398    // The prompt box has just emptied, so the pane can have the keyboard back.
399    await $.ui.open(OPEN)
400
401    return { drop: REFUSAL }
402  }).catch((_, e, next) => next(e))
403
404  // While locked the pane closes for nobody. Escape and the close mark are the
405  // person's close, and refusing it is what keeps Escape from stopping the turn.
406  on('ui.close', { id: PANE }, async ($, e, next) =>
407    (await read($, isLocked)) ? { deny: STAYS } : next(e),
408  ).catch((_, e, next) => next(e))
409
410  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
411    if (!(await read($, isLocked))) {
412      return next(e)
413    }
414
415    // The keyboard has just left the pane. A drawing may not write, so the
416    // asking waits for the draw to end.
417    if (hadKeyboard && !e.props.isFocused) {
418      $.clock.after(0, () => void hold($))
419    }
420
421    hadKeyboard = e.props.isFocused
422
423    const { Box, Button, Text } = $.ui.resolve(e)
424    let sign: RenderElement | null = null
425
426    // The padlock is an Svg, which the terminal does not draw.
427    if (e.surface === 'desktop') {
428      const { Svg } = $.ui.resolve(e)
429      sign = (
430        <Svg
431          source={padlock(true)}
432          alt={LOCK_ON}
433          width={LOCK_WIDTH}
434          height={MARK_HEIGHT}
435        />
436      )
437    }
438
439    return (
440      <Box flexDirection="column" rowGap={1}>
441        <Box alignItems="center" columnGap={1}>
442          {sign}
443          <Text bold>Session locked</Text>
444        </Box>
445        <Box>
446          <Button key="unlock" variant="primary" onPress={() => release($)}>
447            Unlock
448          </Button>
449        </Box>
450      </Box>
451    )
452  })
453
454  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
455    // The band is one tree for every mod: each adds its own line and passes the
456    // lines beneath through as they came. Ours goes under what another mod drew;
457    // we never open that up or set it in our row.
458    const beneath = await next(e)
459
460    if (e.surface !== 'desktop' || e.props.hasSurvey) {
461      return beneath
462    }
463
464    const now = await $.clock.now()
465    const list = await read($, limits)
466    const session = windowOf(list, SESSION.kind)
467    const week = windowOf(list, WEEK.kind)
468    const isQuiet = (!session && !week) || (await read($, isHidden))
469
470    if (isQuiet) {
471      return beneath
472    }
473
474    const isShut = await read($, isLocked)
475    const { Box, Button, Svg, Text } = $.ui.resolve(e)
476
477    const meter = (one: typeof SESSION, limit: Limit) => {
478      const isOver = hasReset(limit, now)
479      const used = isOver ? 0 : limit.percentUsed
480      const percent = Math.round(used)
481      const reset = isOver ? 'reset' : remaining(limit.resetsAt, now)
482      const left =
483        limit.resetsAt === undefined
484          ? 0
485          : share((Date.parse(limit.resetsAt) - now) / one.span)
486      const colour = toneOf(percent)
487      const alt =
488        reset === ''
489          ? `${one.label}: ${percent}% used`
490          : `${one.label}: ${percent}% used, ${reset}`
491
492      return (
493        <Box alignItems="center" columnGap={1}>
494          <Svg
495            source={mark(one.icon, share(used / 100), left, colour)}
496            alt={alt}
497            width={MARK_WIDTH}
498            height={MARK_HEIGHT}
499          />
500          <Text bold color={colour}>
501            {`${percent}%`}
502          </Text>
503        </Box>
504      )
505    }
506
507    const ownLine = (
508      <Box alignItems="center" columnGap={3} flexWrap="wrap">
509        {session && meter(SESSION, session)}
510        {week && meter(WEEK, week)}
511        <Box>
512          <Svg
513            source={padlock(isShut)}
514            alt={isShut ? LOCK_ON : LOCK_OFF}
515            width={LOCK_WIDTH}
516            height={MARK_HEIGHT}
517          />
518          <Box
519            position="absolute"
520            top={0}
521            bottom={0}
522            left={-REACH}
523            right={-REACH}
524            alignItems="center"
525            justifyContent="center"
526          >
527            <Button
528              key="lock"
529              label={BLANK}
530              plain
531              onPress={async () =>
532                (await read($, isLocked)) ? release($) : engage($)
533              }
534            />
535          </Box>
536        </Box>
537      </Box>
538    )
539
540    if (!hasContent(beneath)) {
541      return ownLine
542    }
543
544    return (
545      <Box flexDirection="column" rowGap="md">
546        {beneath}
547        {ownLine}
548      </Box>
549    )
550  })
551}
552
types/index.d.ts 13 lines
1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'usage-band': {
6      limits: Limit[]
7      isHidden: boolean
8      isLocked: boolean
9      draft: string
10    }
11  }
12}
13