SLOPSHOPPER

status-band

Shows context fill and usage limits in the footer under the prompt in the desktop app, where no status line exists

newspinnerguardtimer
★ 2v0.1.0MITupdated 2026-10-03MichaelP17/claude-mods/status-band
A shopper browsing a rack in a slop shop
README

status-band

Shows context and usage limits in the desktop app, where Claude Code has no status line. One short line sits in the footer under the prompt, beside the app's model and effort pickers:

C 10% · 5h 74% · W 98%
PartMeaning
CHow full the context window is
5hUse of the five-hour rate-limit window
WUse of the weekly rate-limit window

Each value is green below 60%, yellow from 60% and red from 85%. A window without a reading, as off a subscription, is left out. Mode labels the app shows in the same footer stay in front.

Mode, folder, model and effort are not repeated: the app's own controls already show them. In the terminal the mod draws nothing, so an existing status line is not duplicated.

When it updates

After every tool call and turn, when the desktop app connects, and every 30 seconds.

Source 3 files
hooks/register.tsx 96 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Usage } from '../types'
5import { spansOf } from './format'
6
7const REFRESH_MS = 30_000
8
9const usage = atom({ plugin: 'status-band', key: 'usage' } as const, null)
10
11async function isDrawnRemotely($: EngineInterface): Promise<boolean> {
12  const surfaces = await $.session.surfaces()
13
14  return surfaces.some(i => i !== 'terminal')
15}
16
17async function refresh($: EngineInterface): Promise<void> {
18  if (!(await isDrawnRemotely($))) {
19    return
20  }
21  const current = await $.session.usage()
22  const windowOf = (kind: string) => current.rateLimits.find(i => i.kind === kind)?.percentUsed ?? null
23  const next: Usage = {
24    contextPercent: current.context.percent ?? null,
25    fiveHourPercent: windowOf('five_hour'),
26    sevenDayPercent: windowOf('seven_day'),
27  }
28  await update($, usage, () => next)
29}
30
31export const register: Register = on => {
32  on('session.start', async ($, e, next) => {
33    const result = await next(e)
34    $.clock.every(REFRESH_MS, () => {
35      void refresh($)
36    })
37    await refresh($)
38
39    return result
40  })
41
42  on('session.attach', async ($, e, next) => {
43    const result = await next(e)
44    await refresh($)
45
46    return result
47  })
48
49  on('tool.call', async ($, e, next) => {
50    const result = await next(e)
51    if (e.agentId === undefined) {
52      await refresh($)
53    }
54
55    return result
56  })
57
58  on('turn.complete', async ($, e, next) => {
59    const result = await next(e)
60    await refresh($)
61
62    return result
63  })
64
65  // The desktop draws the SessionMode labels in the footer under the prompt,
66  // beside its model and effort pickers: the one slot with room for a short line.
67  // Other mods may draw there too; what they drew beneath stays.
68  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
69    if (e.surface === 'terminal') {
70      return next(e)
71    }
72    const current = await read($, usage)
73    const spans = current === null ? [] : spansOf(current)
74    const below = await next(e)
75    if (spans.length === 0) {
76      return below
77    }
78    const { Box, Text } = $.ui.resolve(e)
79    const modes = e.props.modes.length === 0 ? null : <Text dimColor>{`${e.props.modes.join(' & ')} · `}</Text>
80
81    return (
82      <Box flexDirection="row">
83        {below.type === 'engine' ? modes : below}
84        {below.type === 'engine' ? null : <Text dimColor> · </Text>}
85        <Text wrap="truncate-end">
86          {spans.map((span, index) => (
87            <Text key={`span-${index}`} color={span.color} dimColor={span.isDim}>
88              {span.text}
89            </Text>
90          ))}
91        </Text>
92      </Box>
93    )
94  })
95}
96
hooks/format.ts 39 lines
1import type { Span, Usage } from '../types'
2
3export const COLORS = {
4  green: '#22a355',
5  yellow: '#d4a017',
6  red: '#e5484d',
7}
8
9export function usageColor(percent: number): string {
10  if (percent >= 85) {
11    return COLORS.red
12  }
13  if (percent >= 60) {
14    return COLORS.yellow
15  }
16
17  return COLORS.green
18}
19
20export function spansOf(usage: Usage): Span[] {
21  const readings: [string, number | null][] = [
22    ['C', usage.contextPercent],
23    ['5h', usage.fiveHourPercent],
24    ['W', usage.sevenDayPercent],
25  ]
26  const spans: Span[] = []
27  for (const [label, percent] of readings) {
28    if (percent === null) {
29      continue
30    }
31    if (spans.length > 0) {
32      spans.push({ text: ' · ', isDim: true })
33    }
34    spans.push({ text: `${label} `, isDim: true }, { text: `${Math.round(percent)}%`, color: usageColor(percent) })
35  }
36
37  return spans
38}
39
types/index.d.ts 20 lines
1export type Usage = {
2  contextPercent: number | null
3  fiveHourPercent: number | null
4  sevenDayPercent: number | null
5}
6
7export type Span = {
8  text: string
9  color?: string
10  isDim?: boolean
11}
12
13declare module 'claude-code' {
14  interface PluginState {
15    'status-band': {
16      usage: Usage | null
17    }
18  }
19}
20