A pane showing context fill, session token totals, cost and rate limits

A personal Claude Code plugin marketplace of terminal UI mods: a status band above the prompt and two side panes.
| Plugin | What it adds | Command |
|---|---|---|
prompt-status | A two-line band above the prompt: model and context window, current directory, git branch, context fill and rate-limit usage | — |
token-usage | A pane with context fill, session token totals, cost and rate limits | /token-usage |
git-changes | A pane listing the files changed in the working tree, with colored git diff stats | /changes |
/plugin marketplace add matr1xp/local-mods
/plugin install prompt-status@local-mods
/plugin install token-usage@local-mods
/plugin install git-changes@local-mods
Renders above the prompt in every session:

model setting.*) refresh after each turn and once a minute. A detached HEAD shows its short commit hash.Opens a pane at session start. Run /token-usage to reopen it. The pane shows:

Running /clear resets the session totals.
Opens a pane in interactive sessions. Run /changes to reopen it and print a summary. The pane lists every file that differs from HEAD, whether staged, unstaged or untracked:

M, A, D, R, ??, …), +added / -deleted counts and a git --stat-style bar. Binary files show bin.● marker on files whose stats changed since the previous scan.Edit, Write, NotebookEdit and Bash tool calls, and every 4 seconds while it is on screen. Press r to refresh by hand.GIT_OPTIONAL_LOCKS=0, so polling never holds index.lock while you run your own git commands.new..claude-plugin/marketplace.json marketplace manifest listing the plugins
<plugin>/
.claude-plugin/plugin.json name, version, description
hooks/hooks.json points at the hooks module
hooks/register.tsx the mod: event hooks and UI rendering
types/index.d.ts shapes of the state each mod stores
tests/*.test.ts(x) tests using claude-code/testing
Run a plugin's tests, then validate the marketplace:
claude plugin test git-changes
claude plugin validate .
After changing a plugin, bump version in its .claude-plugin/plugin.json so other machines pick up the update with claude plugin update <plugin>@local-mods.
MIT. The software is provided as is, without warranty of any kind, and the author is not liable for any damage arising from its use.
hooks/register.tsx 196 lines1import { atom, read, update } from 'claude-code'
2import type { Register, SessionMeasureInput, SessionUsage } from 'claude-code'
3
4import type { LastRequest, Snapshot, Totals } from '../types'
5
6const PANE = 'token-usage'
7const TITLE = 'Token usage'
8// Widest line is the rate-limit row: "5h ████████████ 23.5% ↻14:00".
9const COLUMNS = 34
10const OPEN = { id: PANE, title: TITLE, columns: COLUMNS } as const
11
12const EMPTY: Totals = {
13 input: 0,
14 output: 0,
15 cacheRead: 0,
16 cacheWrite: 0,
17 requests: 0,
18 subagentRequests: 0,
19}
20
21const totals = atom({ plugin: 'token-usage', key: 'totals' } as const, EMPTY)
22const last = atom({ plugin: 'token-usage', key: 'last' } as const, null)
23const snapshot = atom({ plugin: 'token-usage', key: 'snapshot' } as const, null)
24
25const toSnapshot = (u: SessionUsage | SessionMeasureInput): Snapshot => ({
26 contextTokens: u.context.tokens,
27 contextWindow: u.context.window,
28 contextPercent: u.context.percent,
29 rateLimits: u.rateLimits.map(({ kind, percentUsed, resetsAt }) => ({
30 kind,
31 percentUsed,
32 resetsAt,
33 })),
34 costUsd: u.cost?.usd,
35})
36
37const fmt =(n: number): string => {
38 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1)}M`
39 if (n >= 1_000) return `${(n / 1_000).toFixed(n >= 10_000 ? 0 : 1)}k`
40 return String(n)
41}
42
43const bar = (percent: number, width: number): string => {
44 const filled = Math.round((Math.min(100, Math.max(0, percent)) / 100) * width)
45 return '█'.repeat(filled) + '░'.repeat(width - filled)
46}
47
48const LIMIT_NAMES: Record<string, string> = {
49 five_hour: '5h',
50 seven_day: '7d',
51 spend_limit: 'spend',
52}
53
54const resetTime = (iso?: string): string => {
55 if (!iso) return ''
56 const d = new Date(iso)
57 if (Number.isNaN(d.getTime())) return ''
58 const hh = String(d.getHours()).padStart(2, '0')
59 const mm = String(d.getMinutes()).padStart(2, '0')
60 return ` ↻${hh}:${mm}`
61}
62
63const levelColor = (percent: number) =>
64 percent >= 85 ? 'error' : percent >= 60 ? 'warning' : 'success'
65
66export const register: Register = on => {
67 on('session.start', async ($, e, next) => {
68 await $.command.register({
69 name: 'token-usage',
70 description: 'Show token usage, cost and rate limits in a pane',
71 })
72 const usage = await $.session.usage()
73 await update($, snapshot, () => toSnapshot(usage))
74 void $.ui.open(OPEN)
75
76 return next(e)
77 })
78
79 on('command.run', { command: 'token-usage' }, async $ => {
80 await $.ui.open(OPEN)
81
82 return { text: 'Token usage pane opened.' }
83 })
84
85 on('turn.step', async function* ($, e, next) {
86 const result = yield* next(e)
87 const usage = result?.usage
88 if (usage) {
89 const isSubagent = e.agentId !== undefined
90 await update($, totals, t => ({
91 input: t.input + usage.input_tokens,
92 output: t.output + usage.output_tokens,
93 cacheRead: t.cacheRead + usage.cache_read_input_tokens,
94 cacheWrite: t.cacheWrite + usage.cache_creation_input_tokens,
95 requests: t.requests + 1,
96 subagentRequests: t.subagentRequests + (isSubagent ? 1 : 0),
97 }))
98 const request: LastRequest = {
99 model: usage.model,
100 isSubagent,
101 input: usage.input_tokens,
102 output: usage.output_tokens,
103 cacheRead: usage.cache_read_input_tokens,
104 cacheWrite: usage.cache_creation_input_tokens,
105 }
106 await update($, last, () => request)
107 }
108
109 return result
110 })
111
112 on('session.measure', async ($, e, next) => {
113 await update($, snapshot, () => toSnapshot(e))
114
115 return next(e)
116 })
117
118 on('session.end', async ($, e, next) => {
119 if (e.reason === 'clear') {
120 await update($, totals, () => EMPTY)
121 await update($, last, () => null)
122 }
123
124 return next(e)
125 })
126
127 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
128 const { Box, Text } = $.ui.resolve(e)
129 const t = await read($, totals)
130 const l = await read($, last)
131 const s = await read($, snapshot)
132 // Context row is "<bar> 100%"; limit rows add a 6-cell label and a reset time.
133 const width = Math.max(8, Math.min(24, e.props.bodyColumns - 6))
134 const limitWidth = Math.max(6, Math.min(12, e.props.bodyColumns - 19))
135
136 const percent = s?.contextPercent
137 const hasContext = s !== null && percent !== undefined
138
139 return (
140 <Box flexDirection="column">
141 <Text bold>Context</Text>
142 {hasContext ? (
143 <Box flexDirection="column">
144 <Text color={levelColor(percent)}>
145 {bar(percent, width)} {percent}%
146 </Text>
147 <Text dimColor>
148 {fmt(s.contextTokens ?? 0)} / {fmt(s.contextWindow)} tokens
149 </Text>
150 </Box>
151 ) : (
152 <Text dimColor>No reading yet</Text>
153 )}
154
155 <Text> </Text>
156 <Text bold>Session</Text>
157 <Text>
158 in {fmt(t.input)} · out {fmt(t.output)}
159 </Text>
160 <Text dimColor>
161 cache read {fmt(t.cacheRead)} · write {fmt(t.cacheWrite)}
162 </Text>
163 <Text dimColor>
164 {t.requests} requests
165 {t.subagentRequests > 0 ? ` (${t.subagentRequests} subagent)` : ''}
166 </Text>
167 {s?.costUsd !== undefined && <Text>cost ${s.costUsd.toFixed(2)}</Text>}
168
169 {l !== null && (
170 <Box flexDirection="column">
171 <Text> </Text>
172 <Text bold>Last request{l.isSubagent ? ' (subagent)' : ''}</Text>
173 <Text dimColor>{l.model}</Text>
174 <Text>
175 in {fmt(l.input)} · out {fmt(l.output)} · cached {fmt(l.cacheRead)}
176 </Text>
177 </Box>
178 )}
179
180 {s !== null && s.rateLimits.length > 0 && (
181 <Box flexDirection="column">
182 <Text> </Text>
183 <Text bold>Rate limits</Text>
184 {s.rateLimits.map(r => (
185 <Text color={levelColor(r.percentUsed)}>
186 {(LIMIT_NAMES[r.kind] ?? r.kind).padEnd(5)} {bar(r.percentUsed, limitWidth)}{' '}
187 {r.percentUsed}%{resetTime(r.resetsAt)}
188 </Text>
189 ))}
190 </Box>
191 )}
192 </Box>
193 )
194 })
195}
196types/index.d.ts 31 lines1export type Tokens = {
2 input: number
3 output: number
4 cacheRead: number
5 cacheWrite: number
6}
7
8export type Totals = Tokens & { requests: number; subagentRequests: number }
9
10export type LastRequest = Tokens & { model: string; isSubagent: boolean }
11
12export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
13
14export type Snapshot = {
15 contextTokens?: number
16 contextWindow: number
17 contextPercent?: number
18 rateLimits: Limit[]
19 costUsd?: number
20}
21
22declare module 'claude-code' {
23 interface PluginState {
24 'token-usage': {
25 totals: Totals
26 last: LastRequest | null
27 snapshot: Snapshot | null
28 }
29 }
30}
31