A status line above the prompt: model, dir@branch, diff counts, context window usage and the 5-hour limit

A Claude Code mod that draws a status line above the prompt, in the terminal and in the Desktop app's Code tab.
Opus 5.5 | myproject@main (+13 -3) | 84k/200k (42%) | 5h 37%
| Segment | Meaning | | :- | :- | | Opus 5.5 | The session's model, as /model shows it | | myproject@main | The session directory's name and its git branch; the branch is left out outside a repository | | (+13 -3) | Lines added and deleted in unstaged changes; left out when the tree is clean | | 84k/200k (42%) | Context tokens used, the model's window, and the percentage | | 5h 37% | How much of the 5-hour limit is used; 5h -- until the session's first response |
The colors are Solarized's accents, which read on a light and on a dark background. Both percentages turn orange at 70% and red at 90%.
The line is drawn again after each model response and whenever Claude Code measures the session. Git is read in the background, one read at a time, so a slow repository never holds a turn or the session's start.
git on your PATH, for the branch and the line countsThe mod draws in the terminal and in the Desktop app. Its hooks run elsewhere (the VS Code extension's chat panel, claude -p), but nothing is drawn there.
In a Claude Code session in your terminal:
/plugin install statusline-band --marketplace waylongo/statusline-band
Or from your shell:
claude plugin marketplace add waylongo/statusline-band
claude plugin install statusline-band@statusline-band
Installed at the user scope from a terminal, it loads in the Desktop app's local sessions too. Sessions that are already open pick it up after /reload-plugins or a restart.
A mod is code that runs with your permissions, so here is all this one does:
git rev-parse --abbrev-ref HEAD and git diff --numstat in the session's directory. Both only read.You can check that before installing. Clone the repository and run:
claude plugin validate ./statusline-band
The hooks: and calls: lines list every event the mod handles and everything it asks Claude Code to do.
5h --.git add.HEAD.Checked by hand in the macOS Desktop app. The terminal is covered by the test suite only.
Everything is in hooks/register.tsx, about 150 lines. The colors are the constants at the top, and colorFor holds the 70% and 90% thresholds.
claude --plugin-dir . # load from this folder; saving a file reloads the mod
claude plugin validate . # what the engine would refuse, before a session loads it
claude plugin test . # run tests/*.test.tsx against the engine
Claude Code writes its type declarations to .claude-plugin/types/ each time it loads the mod from this folder, which is what tsconfig.json extends.
一个 Claude Code mod:在输入框上方画一条状态栏,终端和桌面端 Code 标签页都能显示。
从左到右依次是:模型名、目录名@git 分支、未暂存改动的增删行数、上下文已用/窗口大小(百分比)、5 小时限额已用百分比。新会话在第一次回复之前没有限额读数,显示 5h --。两个百分比到 70% 变橙色,到 90% 变红色。
安装(在终端的 Claude Code 会话里输入):
/plugin install statusline-band --marketplace waylongo/statusline-band
需要 Claude Code v2.1.287 以上(桌面端 v2.1.286 以上)。它只运行两条只读的 git 命令,不联网、不写文件。已知限制见上面的 Known limitations。
hooks/register.tsx 150 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// Solarized's accents, which read on a light and on a dark background
4const BLUE = '#268bd2'
5const CYAN = '#2aa198'
6const GREEN = '#859900'
7const YELLOW = '#b58900'
8const ORANGE = '#cb4b16'
9const RED = '#dc322f'
10
11type Git = { branch: string; adds: number; dels: number }
12
13// Read again at session.start, which a reload raises too
14let git: Git | null = null
15let isReadingGit = false
16let isGitStale = false
17
18const compact = (tokens: number) =>
19 tokens >= 1_000_000
20 ? `${+(tokens / 1_000_000).toFixed(1)}m`
21 : tokens >= 1_000
22 ? `${Math.round(tokens / 1_000)}k`
23 : String(tokens)
24
25const colorFor = (percent: number) =>
26 percent >= 90 ? RED : percent >= 70 ? ORANGE : GREEN
27
28const readGit = async ($: EngineInterface): Promise<Git | null> => {
29 const head = await $.process.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
30
31 if (head.exitCode !== 0) {
32 return null
33 }
34
35 const diff = await $.process.run(['git', 'diff', '--numstat'])
36 let adds = 0
37 let dels = 0
38
39 for (const line of diff.stdout.split('\n')) {
40 // A binary file's counts are "-"
41 const [added, deleted] = line.split('\t')
42 adds += Number(added) || 0
43 dels += Number(deleted) || 0
44 }
45
46 return { branch: head.stdout.trim(), adds, dels }
47}
48
49// One read at a time: a call made meanwhile asks for one more read after it
50const refreshGit = async ($: EngineInterface) => {
51 if (isReadingGit) {
52 isGitStale = true
53
54 return
55 }
56
57 isReadingGit = true
58
59 try {
60 do {
61 isGitStale = false
62 git = await readGit($).catch(() => null)
63 $.ui.invalidate('ui.render')
64 } while (isGitStale)
65 } finally {
66 isReadingGit = false
67 }
68}
69
70export const register: Register = on => {
71 on('session.start', ($, e, next) => {
72 // Not awaited: a slow git must not hold the session's start
73 void refreshGit($)
74
75 return next(e)
76 })
77
78 on('turn.step', async function* ($, e, next) {
79 const response = yield* next(e)
80 $.ui.invalidate('ui.render')
81 // Not awaited: a slow git must not hold the turn
82 void refreshGit($)
83
84 return response
85 })
86
87 on('session.measure', ($, e, next) => {
88 $.ui.invalidate('ui.render')
89
90 return next(e)
91 })
92
93 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
94 if (e.props.hasSurvey) {
95 return next(e)
96 }
97
98 const [model, cwd, { context, rateLimits }] = await Promise.all([
99 $.session.model(),
100 $.session.cwd(),
101 $.session.usage(),
102 ])
103 const { Box, Text } = $.ui.resolve(e)
104 const percent = context.percent ?? 0
105 const fiveHour = rateLimits.find(limit => limit.kind === 'five_hour')
106
107 const row = [
108 <Text color={BLUE}>{model}</Text>,
109 <Text dimColor> | </Text>,
110 <Text color={CYAN}>{cwd.slice(cwd.lastIndexOf('/') + 1)}</Text>,
111 ]
112
113 if (git !== null) {
114 row.push(<Text dimColor>@</Text>, <Text color={GREEN}>{git.branch}</Text>)
115
116 if (git.adds + git.dels > 0) {
117 row.push(<Text dimColor> (</Text>)
118 if (git.adds > 0) row.push(<Text color={GREEN}>+{git.adds}</Text>)
119 if (git.adds > 0 && git.dels > 0) row.push(<Text> </Text>)
120 if (git.dels > 0) row.push(<Text color={RED}>-{git.dels}</Text>)
121 row.push(<Text dimColor>)</Text>)
122 }
123 }
124
125 row.push(
126 <Text dimColor> | </Text>,
127 <Text color={YELLOW}>
128 {compact(context.tokens ?? 0)}/{compact(context.window)}
129 </Text>,
130 <Text dimColor> (</Text>,
131 <Text color={colorFor(percent)}>{percent}%</Text>,
132 <Text dimColor>)</Text>,
133 )
134
135 row.push(
136 <Text dimColor> | 5h </Text>,
137 // No reading before the session's first response
138 fiveHour === undefined ? (
139 <Text dimColor>--</Text>
140 ) : (
141 <Text color={colorFor(fiveHour.percentUsed)}>
142 {Math.round(fiveHour.percentUsed)}%
143 </Text>
144 ),
145 )
146
147 return <Box>{...row}</Box>
148 })
149}
150