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

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.context can be switched off with its show* option.hooks/register.tsx 135 lines1import { 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}
135types/index.d.ts 16 lines1export 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