SLOPSHOPPER

session-status

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

newspinnertimer
v0.1.0MITupdated 2026-10-19monajjar1/claude-session-status
A shopper browsing a rack in a slop shop
README

session-status

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
  • Model and context fill for the current window
  • Current session usage limit (5-hour window) with time until reset. Only shown on a Claude subscription.
  • Session name: the name other sessions use to message this one

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.

Install

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.

Local models

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.

Develop

claude --plugin-dir .           # run a session with this folder loaded
claude plugin validate .
claude plugin test .

License

MIT

Source 2 files
hooks/register.ts 89 lines
1import { 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}
89
types/index.d.ts 7 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'session-status': { line: string }
4  }
5}
6export type Line = string
7