Footer: model, context fill, current-session usage limit with reset time, session name

A Claude Code mod that adds a live usage line to the right end of the prompt footer:
claude-opus-5-5 · 45,000 / 1,000,000 ctx (5%) · current session ████░ 81% reset in 1h27m · my-session
It updates after every turn, whenever the usage limit moves a point, and once a minute so the countdown stays current. Works on Linux, macOS and Windows.
At the prompt in a Claude Code terminal session:
/plugin install session-status --marketplace monajjar1/claude-session-status
Answer y to add the marketplace, then press Enter to install at user scope. It's active right away.
If CLAUDE_CODE_MAX_CONTEXT_TOKENS is set (for example, when pointing Claude Code at LM Studio or llama.cpp), that value is used as the context window size.
claude --plugin-dir . # run a session with this folder loaded
claude plugin validate .
claude plugin test .
MIT
hooks/register.ts 89 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface as Engine, Register, SessionMeasureInput, SessionRateLimit } from 'claude-code'
3
4const line = atom({ plugin: 'session-status', key: 'line' } as const, '')
5let last: SessionMeasureInput | undefined
6
7export function num(n: number) {
8 return String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
9}
10
11export function until(iso: string, now: number) {
12 const m = Math.max(0, Math.round((Date.parse(iso) - now) / 60000))
13 const d = Math.floor(m / 1440), h = Math.floor((m % 1440) / 60)
14 return d ? `${d}d${h}h` : h ? `${h}h${m % 60}m` : `${m}m`
15}
16
17export function limit(r: SessionRateLimit, now: number) {
18 const filled = Math.round((Math.min(r.percentUsed, 100) / 100) * 5)
19 const reset = r.resetsAt ? ` reset in ${until(r.resetsAt, now)}` : ''
20 return `current session ${'█'.repeat(filled)}${'░'.repeat(5 - filled)} ${Math.round(r.percentUsed)}%${reset}`
21}
22
23// Claude Code registers each running session in ~/.claude/sessions/<pid>.json; the name is what SendMessage uses.
24// The plugin lives under ~/.claude (plugins/cache or a mods folder), so that dir is found from its root.
25async function sessionName($: Engine) {
26 const at = $.plugin.root.search(/[\\/]\.claude[\\/]/) // Windows paths use backslashes
27 if (at < 0) return
28 const dir = `${$.plugin.root.slice(0, at + 8)}/sessions`
29 const sid = await $.session.id()
30 for (const f of await $.fs.list(dir).catch(() => [])) {
31 if (!f.name.endsWith('.json')) continue
32 const s = await $.fs.read(`${dir}/${f.name}`).then(t => JSON.parse(t)).catch(() => null)
33 if (s?.sessionId === sid) return s.name as string
34 }
35}
36
37async function draw($: Engine) {
38 if (!last) return
39 const [model, now, maxEnv, name] = await Promise.all([
40 $.session.model(), $.clock.now(), $.env.get('CLAUDE_CODE_MAX_CONTEXT_TOKENS'), sessionName($),
41 ])
42 // `ai` (~/.local/bin/ai.sh) sets the local model's real window; otherwise the engine's.
43 const max = Number(maxEnv) || last.context.window
44 const used = last.context.tokens ?? 0
45 const text = [
46 model,
47 `${num(used)} / ${num(max)} ctx (${Math.round((used * 100) / max)}%)`,
48 ...last.rateLimits.filter(r => r.kind === 'five_hour').map(r => limit(r, now)),
49 name,
50 ].filter(Boolean).join(' · ')
51 await update($, line, () => text)
52}
53
54// session.measure only fires after a turn: measure at startup (nothing sent yet, e.g. a local model) and after a compaction.
55async function remeasure($: Engine, tokens?: number) {
56 const u = await $.session.usage({ breakdown: 'summary' }).catch(() => undefined)
57 if (!u) return
58 last = { ...u, changed: ['context'], context: { ...u.context, tokens: tokens ?? u.context.breakdown?.totalTokens ?? u.context.tokens } }
59 await draw($)
60}
61
62export const register: Register = on => {
63 on('session.start', async ($, e, next) => {
64 const r = await next(e)
65 $.clock.every(60_000, () => draw($)) // keeps the reset countdown fresh between turns
66 await remeasure($)
67 return r
68 })
69
70 on('session.compact', async ($, e, next) => {
71 const r = await next(e)
72 // precompute only prepares a summary; a subagent's compaction isn't the main context
73 if (e.trigger !== 'precompute' && !e.agentId && r.skip === undefined) await remeasure($, r.tokensAfter)
74 return r
75 })
76
77 on('session.measure', async ($, e, next) => {
78 last = e
79 await draw($)
80 return next(e)
81 })
82
83 // The footer's right-hand labels (`focus`, ...): the line sits at the end of the row.
84 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
85 const text = await read($, line)
86 return next(text ? { ...e, props: { ...e.props, modes: [...e.props.modes, text] } } : e)
87 })
88}
89types/index.d.ts 7 lines1declare module 'claude-code' {
2 interface PluginState {
3 'session-status': { line: string }
4 }
5}
6export type Line = string
7