/sessions: pane listing this project's past sessions from prompt history

Two Claude Code mods:
usage-band: a compact, responsive band above the prompt with your context fill, 5h and 7d rate limits, prompt-cache state, cost, and a few extras.sessions: /sessions opens a pane listing this project's past sessions (short id, age, prompt count, first prompt), read from your prompt history.Looks best with a Nerd Font. The icons and rounded bar ends need one, and they are off by default: without a Nerd Font the band still works, with words for labels and half-block ends. A mod cannot install or select a font, so you do that yourself:
brew install --cask font-jetbrains-mono-nerd-font # macOS; any Nerd Font works
then set your terminal's font to it (for Terminal.app: Settings → Profiles → Font) and turn on the icons option below.
Built and tested on Claude Code 2.1.289, macOS, in a terminal. Mods are a new API, so expect it to move.
| Segment | Meaning |
|---|---|
ctx | Context window fill, a green→red ramp (one shade per 10%), a notch at 80%, and used/window when there is room |
5h, 7d | Rate-limit use as one solid bar each. The track behind is shaded by elapsed time: days (or hours) already gone are lighter, the current one lightest. ⟳ is time to reset |
→100% 1h54m | At your current pace you reach 100% of that window in 1h54m, before it resets. Shown only after 15% of the window has passed |
| sparkline | Burn rate: the 5h window in 12 slices, each as tall as the share of the limit used in it. Kept between sessions in the plugin store |
$ | Session cost |
| cache | Prompt-cache lifetime left (green, yellow under 20%) and hit ratio, or cold with how many tokens the next message re-caches |
pt, branch, PR, jev | Tiny group: ponytail mode, git branch (* = uncommitted) and PR number, jev-model-router status, last skill |
↓ ◈ | Output tokens and cached tokens since the mod loaded |
The band measures itself. It tries the richest layout first and, when the terminal is narrow, folds branch/PR/jev/skill into the tiny group, drops the token counts, and finally simplifies the bars. Core segments (ctx, 5h, 7d, cost, cache) stay as long as they can.
git clone https://github.com/cmbaldwin/claude-usage-band ~/claude-usage-band
In ~/.claude/settings.json:
{
"env": {
"CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1",
"CLAUDE_CODE_PLUGIN_DIRS": "/Users/you/claude-usage-band/usage-band:/Users/you/claude-usage-band/sessions"
}
}
Use absolute paths; CLAUDE_CODE_PLUGIN_DIRS is colon-separated. (CLAUDE_CODE_ENABLE_FUNCTION_HOOKS is in my own settings; I have not checked whether newer builds still need it.) Start a new session.
Claude Code only sends prompt_cache to a status line command, not to mods, so the band reads it from a small file written by a status line script. Needs jq.
{
"statusLine": {
"type": "command",
"command": "bash /Users/you/claude-usage-band/scripts/cache-bridge.sh",
"refreshInterval": 30
}
}
The script prints nothing; it writes ~/.claude/state/cache/<session-id>.json (files older than two days are deleted). If you already have a status line, read stdin once and feed both:
input=$(cat)
printf '%s' "$input" | bash /Users/you/claude-usage-band/scripts/cache-bridge.sh
printf '%s' "$input" | your-existing-command
The cache segment needs Claude Code 2.1.251 or later. Without the script it is simply absent.
Set under pluginConfigs in settings.json, keyed by plugin name:
{
"pluginConfigs": {
"usage-band": { "options": { "icons": true, "hideStatus": true } }
}
}
icons (default false): Nerd Font glyphs and rounded bar ends. Set your terminal font to a Nerd Font first, or you get boxes. Without it the labels are words and the ends are half blocks.hideStatus (default false): clear every status line other plugins set ($.ui.status), so the bottom row shows only the mode labels. The band still shows the jev status either way.Raster element. I have only used the band in a terminal.git and gh; PR lookups run only when the branch changes or every five minutes.Each mod has claude plugin validate <folder> and claude plugin test <folder>. Layout, color and history logic are covered by tests; the visuals are not.
MIT licensed.
hooks/register.tsx 50 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { ago, group } from './format'
5
6const PANE = 'sessions'
7
8// Claude Code's config folder: CLAUDE_CONFIG_DIR, else ~/.claude
9async function claudeDir($: EngineInterface) {
10 return (await $.env.get('CLAUDE_CONFIG_DIR')) ?? `${await $.env.get('HOME')}/.claude`
11}
12const rows = atom({ plugin: 'sessions', key: 'rows' } as const, [])
13
14export const register: Register = on => {
15 on('session.start', async ($, e, next) => {
16 await $.command.register({ name: 'sessions', description: "This project's past sessions, from prompt history" })
17 return next(e)
18 })
19
20 on('command.run', { command: 'sessions' }, async $ => {
21 const cwd = await $.session.cwd()
22 const history = await $.fs.read(`${await claudeDir($)}/history.jsonl`).catch(() => '')
23 const list = group(history, cwd)
24 await update($, rows, () => list)
25 await $.ui.open({ id: PANE, title: 'Sessions' })
26 return { text: list.length ? `${list.length} sessions in ${cwd}. Resume one with: claude --resume <id>` : `No past sessions found for ${cwd}.` }
27 })
28
29 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
30 const { Box, Text } = $.ui.resolve(e)
31 const list = await read($, rows)
32 const now = await $.clock.now()
33 const width = Math.max(20, e.props.bodyColumns - 24)
34
35 return (
36 <Box flexDirection="column">
37 {list.length === 0 && <Text dimColor>No past sessions.</Text>}
38 {list.map(r => (
39 <Box key={r.id} gap={1}>
40 <Text dimColor>{r.id.slice(0, 8)}</Text>
41 <Text color="cyan">{ago(r.last, now).padStart(7)}</Text>
42 <Text dimColor>{String(r.prompts).padStart(3)}p</Text>
43 <Text>{r.first.length > width ? `${r.first.slice(0, width - 1)}…` : r.first || '(no prompt)'}</Text>
44 </Box>
45 ))}
46 </Box>
47 )
48 })
49}
50hooks/format.ts 30 lines1import type { Row } from '../types'
2
3type Entry = { display: string; timestamp: number; project: string; sessionId: string }
4
5// Group one project's history entries into sessions, newest first.
6export const group = (jsonl: string, project: string, limit = 20): Row[] => {
7 const by = new Map<string, Row>()
8 for (const line of jsonl.split('\n')) {
9 let e: Entry
10 try { e = JSON.parse(line) } catch { continue }
11 if (e.project !== project || !e.sessionId) continue
12 const text = e.display.trim()
13 // bare "exit" and "/usage" make poor titles
14 const isNoise = text === 'exit' || text.startsWith('/usage')
15 const r = by.get(e.sessionId) ?? { id: e.sessionId, first: '', last: 0, prompts: 0 }
16 if (!r.first && !isNoise) r.first = text.replace(/\s+/g, ' ')
17 r.last = Math.max(r.last, e.timestamp)
18 r.prompts += 1
19 by.set(e.sessionId, r)
20 }
21 return [...by.values()].sort((a, b) => b.last - a.last).slice(0, limit)
22}
23
24export const ago = (ms: number, now: number) => {
25 const m = Math.max(0, Math.round((now - ms) / 60000))
26 const h = Math.floor(m / 60)
27 const d = Math.floor(h / 24)
28 return d ? `${d}d ago` : h ? `${h}h ago` : `${m}m ago`
29}
30types/index.d.ts 8 lines1export type Row = { id: string; first: string; last: number; prompts: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 sessions: { rows: Row[] }
6 }
7}
8