SLOPSHOPPER

usage-band

Band above the prompt showing context %, tools, skills and MCP share of context, and session and weekly limits, with yellow and red usage indicators

newband
v0.1.0Apache-2.0updated 2026-10-05paulosergio-jnr/claude-code-mods/mods/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 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────… context 49.0% · session 31.0% (resets at Thu 02:53) ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
───────────────────────────────────────────────────────────────────────────────────────────────────… context 49.0% · session 31.0% (resets at Thu 02:53)
README

usage-band

A dim band above the prompt:

context 12.0% · tools 3.0% · skills 30 (0.5%) · mcp 15 (0.0%) · session 10.0% (resets at 11:40) · week 5.0% (resets at Wed 00:00)
  • context, session and week turn yellow from 75.0% and red from 90.0%.
  • tools is the loaded tools' share of the context window; skills and mcp show a count and their share. All come from the engine's local estimate (/context may differ slightly).
  • session is the five-hour rate-limit window and week the seven-day one; both only appear on a subscription.
  • Each entry except context can be switched off with its show* option.
Source 2 files
hooks/register.tsx 135 lines
1import { atom, update, read } from 'claude-code'
2import type { Segment } from '../types'
3import type { EngineInterface, Register, SessionContextBreakdown, SessionRateLimit } from 'claude-code'
4
5const text = atom({ plugin: 'usage-band', key: 'text' } as const, null)
6
7// Soft tones, none near the yellow and red of the indicators.
8const COLORS = {
9  context: '#6fc3d6',
10  tools: '#b8bcc8',
11  skills: '#82c796',
12  mcp: '#c58fd9',
13  session: '#7488e0',
14  week: '#8c9eff',
15} as const
16
17// Yellow from 75% and red from 90%, so the label colors keep clear of both.
18const levelOf = (percent: number): 'yellow' | 'red' | undefined => (percent >= 90 ? 'red' : percent >= 75 ? 'yellow' : undefined)
19
20const pct = (tokens: number, window: number): string =>
21  window > 0 ? `${((tokens / window) * 100).toFixed(1)}%` : '–'
22
23type Counts = { tools: number; mcpTools: number }
24
25export type Show = { tools: boolean; skills: boolean; mcp: boolean; session: boolean; week: boolean }
26
27const ALL: Show = { tools: true, skills: true, mcp: true, session: true, week: true }
28
29const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
30
31// The reset time in local time, led by the weekday when it is not today.
32const clock = (iso: string): string => {
33  const d = new Date(iso)
34  const time = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
35  return d.toDateString() === new Date().toDateString() ? time : `${DAYS[d.getDay()]} ${time}`
36}
37
38// A rate-limit window as `label 32% (resets at 14:33)`; absent off a subscription.
39function window(limits: readonly SessionRateLimit[], kind: string): Omit<Segment, 'label' | 'color'> | undefined {
40  const w = limits.find(l => l.kind === kind)
41  if (!w) return undefined
42  return {
43    value: `${w.percentUsed.toFixed(1)}%`,
44    level: levelOf(w.percentUsed),
45    note: w.resetsAt ? `(resets at ${clock(w.resetsAt)})` : undefined,
46  }
47}
48
49const windows = (limits: readonly SessionRateLimit[], show: Show): Segment[] => {
50  const out: Segment[] = []
51  const session = show.session ? window(limits, 'five_hour') : undefined
52  const week = show.week ? window(limits, 'seven_day') : undefined
53  if (session) out.push({ label: 'session', ...session, color: COLORS.session })
54  if (week) out.push({ label: 'week', ...week, color: COLORS.week })
55  return out
56}
57
58// Every figure but the tool count comes from the breakdown. The built-in tools
59// are told apart by the row's name, the one place the engine's labels are read.
60export function line(
61  percent: number | undefined,
62  b: SessionContextBreakdown | undefined,
63  n: Counts,
64  limits: readonly SessionRateLimit[] = [],
65  show: Show = ALL,
66): Segment[] {
67  const parts: Segment[] = [{ label: 'context', value: `${(percent ?? 0).toFixed(1)}%`, level: levelOf(percent ?? 0), color: COLORS.context }]
68  const tail = windows(limits, show)
69  if (!b) return [...parts, ...tail]
70
71  const max = b.rawMaxTokens
72  const toolTokens = b.categories.filter(c => c.kind === 'used' && /^system tools/i.test(c.name)).reduce((s, c) => s + c.tokens, 0)
73  const mcpTokens = b.mcpTools.filter(t => t.isLoaded).reduce((s, t) => s + t.tokens, 0)
74
75  if (show.tools) parts.push({ label: 'tools', value: pct(toolTokens, max), color: COLORS.tools })
76  if (show.skills) parts.push({ label: 'skills', value: `${b.skills?.includedSkills ?? 0} (${pct(b.skills?.tokens ?? 0, max)})`, color: COLORS.skills })
77  if (show.mcp) parts.push({ label: 'mcp', value: `${n.mcpTools} (${pct(mcpTokens, max)})`, color: COLORS.mcp })
78  parts.push(...tail)
79  return parts
80}
81
82async function draw($: EngineInterface, show: Show): Promise<void> {
83  const [usage, tools] = await Promise.all([$.session.usage({ breakdown: 'summary' }), $.tool.list()])
84  const mcpTools = tools.filter(t => t.mcp).length
85  await update($, text, () => line(usage.context.percent, usage.context.breakdown, { tools: tools.length - mcpTools, mcpTools }, usage.rateLimits, show))
86}
87
88// An option left unset, or stored as anything but false, stays on.
89export const showOf = (options: Readonly<Record<string, unknown>>): Show => ({
90  tools: options.showTools !== false,
91  skills: options.showSkills !== false,
92  mcp: options.showMcp !== false,
93  session: options.showSession !== false,
94  week: options.showWeek !== false,
95})
96
97export const register: Register = (on, options) => {
98  const show = showOf(options)
99
100  on('session.start', async ($, e, next) => {
101    await draw($, show)
102    return next(e)
103  })
104
105  on('session.measure', async ($, e, next) => {
106    await draw($, show)
107    return next(e)
108  })
109
110  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
111    const line = await read($, text)
112    if (e.props.hasSurvey || line === null) return next(e)
113
114    const { Box, Text } = $.ui.resolve(e)
115    return (
116      <Box flexDirection="column">
117        <Text dimColor wrap="truncate">
118          {'─'.repeat(500)}
119        </Text>
120        <Text wrap="truncate">
121          {line.map((seg, i) => (
122            <Text key={seg.label}>
123              {i > 0 ? <Text dimColor>{' · '}</Text> : null}
124              <Text color={seg.color}>{seg.label}</Text>
125              {' '}
126              <Text color={seg.level}>{seg.value}</Text>
127              {seg.note ? ` ${seg.note}` : null}
128            </Text>
129          ))}
130        </Text>
131      </Box>
132    )
133  })
134}
135
types/index.d.ts 16 lines
1export type Segment = {
2  label: string
3  value: string
4  color: string
5  /** Set from 75% (yellow) and 90% (red); the value is drawn in the default color otherwise. */
6  level?: 'yellow' | 'red'
7  /** Trailing text in the default color, such as when a window resets. */
8  note?: string
9}
10
11declare module 'claude-code' {
12  interface PluginState {
13    'usage-band': { text: Segment[] | null }
14  }
15}
16