A tachometer for your Claude sessions: when the needle hits the redline, stop spawning and start finishing.

Too many Claudes going at once? Feeling overwhelmed? redline puts a gauge above your Claude Code prompt that counts how many of your sessions are working, so you can plan your time better.
<img width="638" height="98" alt="image" src="https://github.com/user-attachments/assets/c7d1ae26-46c2-4222-be22-d6f85e3db13f" />
<img width="302" height="731" alt="image" src="https://github.com/user-attachments/assets/4e546a80-f59e-4ba1-a81e-f5f83e7b584f" />
stop starting new Claudes and finish the ones already running.
Add the marketplace:
/plugin marketplace add jamubc/toolbox
Install the plugin:
/plugin install redline@toolbox
| Command | What it does |
|---|---|
| UI Mod | gauge & labels. |
/redline | Toggles detailed view. |
/redline <0-25> | demo: enable. |
/redline live | demo: disable |
/config → Show the count at the bottom of the terminal | toggle labels |
/config → Show the fuel gauges | toggle the 5H / 7D / CTX dials |
With Show the fuel gauges on, three more dials ride along: beside the tachometer above your prompt, and under it in the pane.
| Dial | What it reads |
|---|---|
| 5H | Your plan's five-hour rate-limit window. |
| 7D | Your plan's seven-day rate-limit window. |
| CTX | This session's context, filling as the conversation grows. |
On every dial the needle swings right, into the red, as its tank is used. The plan windows drain, so 5H and 7D read F (full) to E (empty); the context fills, so CTX reads E to F. The plan dials need a subscription; CTX works for everyone. A dial the engine has no reading for stays dim.
| Working | Stage |
|---|---|
| 0 | Napping |
| 1–2 | Idling |
| 3–5 | Cruising |
| 6–8 | Busy |
| 9–12 | Very Busy |
| 13–17 | Under Pressure |
| 18–20 | Redline |
| 21–24 | Overboost |
| 25+ | Blown |
redline runs two read-only programs on your machine and sends nothing anywhere. It makes no network requests.
| Program | Exactly what it runs | Why |
|---|---|---|
ps | ps -x -o pid=,ppid=,tty=,args=, every two seconds | Lists your own processes, to find the claude sessions in your terminals and which are mid-turn. |
ps | ps -x -o pid=,ppid=,tty=,command=, once a session until it succeeds | The same table in a second format, so redline can tell which session is this one and mark it (this one) in the pane. |
lsof | lsof -a -d cwd -Fn -p <pids>, every two seconds, where <pids> are the claude sessions ps just found | Reads each session's working directory, shown beside it in the pane. |
claude process attached to a terminal. It counts as working while Claude Code holds a caffeinate child process, which happens on macOS during a turn. On Linux every session reads as idle.session.usage, and the session.measure event after each turn): the context fill and your plan's rate-limit percentages, the same figures the status line has. They are drawn on the dials and kept in this session's plugin state; they are never written to disk by redline or sent anywhere.command.run only for its own /redline command, which opens and closes the pane and sets the demo count. It does not see or change any other command.ui.close to notice when you close its pane, and session.start, session.end and session.measure to keep the gauge right across /clear.hooks/register.tsx 355 lines1// redline: a turbo boost gauge above the prompt, its needle on how many Claude sessions in your
2// terminals are working. `/redline` opens a pane with a larger gauge and every session. With the
3// fuelGauge option on, three more dials ride along: the plan's five-hour and seven-day windows
4// and this session's context, from `$.session.usage()` and the `session.measure` pushes.
5//
6// The render hooks mount Rasters; one 10 fps timer steps the needles and repaints them with
7// `$.ui.blit`, with no render pass. Sessions live in `$.state`, so they survive a hot reload, and
8// in this module too, so they survive a /clear (which empties `$.state`: see `snapshot`).
9
10import { atom, derive, read, update } from 'claude-code'
11import type { EngineInterface, Register } from 'claude-code'
12
13import type { RedlineSession, RedlineUsage } from '../types'
14import { BAND_COLS, BIG, TALL, bandCanvas, fracOf, paneCanvas, paneLayout, tankFrac, usageFrom, type PaneLayout, type Tanks } from './fuel'
15import { LARGE, Needle, SCALE, SMALL, colorFor, gauge, stageFor, type Face } from './gauge'
16import { cwdsIn, lsofArgv, selfIn, sessionsIn } from './sessions'
17
18const PANE = 'redline'
19const FPS = 10
20const POLL_MS = 2000
21const BAND_WORDS = 20 // the columns the band's words need beside the fuel dials
22const EMPTY_USAGE: RedlineUsage = { fiveHour: null, sevenDay: null, context: null }
23
24const sessions = atom({ plugin: 'redline', key: 'sessions' } as const, [] as RedlineSession[])
25const preview = atom({ plugin: 'redline', key: 'preview' } as const, null as number | null)
26const isPaneOpen = atom({ plugin: 'redline', key: 'isPaneOpen' } as const, false)
27const seeded = atom({ plugin: 'redline', key: 'seeded' } as const, false)
28const usage = atom({ plugin: 'redline', key: 'usage' } as const, EMPTY_USAGE)
29
30// /clear ends the session but not the process: the host's `$.state` starts over empty while this
31// module, its pane and its timers live on, so read straight from the host there are suddenly no
32// sessions, no preview and no pane. `seeded` is false exactly then, and before the first
33// `session.start`. `snapshot` is what the drawings read, in one go: the host's while `seeded`,
34// else what this process last saw; `write` puts the kept values back before the first change
35// after a /clear, and a render that finds `seeded` false schedules that.
36type Snapshot = { sessions: RedlineSession[]; preview: number | null; isPaneOpen: boolean; usage: RedlineUsage }
37const KEYS = ['sessions', 'preview', 'isPaneOpen', 'usage'] as const
38let kept: Snapshot = { sessions: [], preview: null, isPaneOpen: false, usage: EMPTY_USAGE }
39let wasLive: boolean | null = null // what the last read saw; null before the first
40const snapshot = derive([seeded, sessions, preview, isPaneOpen, usage], (isLive, sessions, preview, isPaneOpen, usage): Snapshot => {
41 wasLive = isLive
42 if (!isLive) return kept
43 kept = { sessions, preview, isPaneOpen, usage }
44 return kept
45})
46
47/** The one place the atoms are written: each key to its own, as the validator asks. */
48async function put<K extends keyof Snapshot>($: EngineInterface, key: K, value: Snapshot[K]): Promise<void> {
49 switch (key) {
50 case 'sessions': await update($, sessions, () => value as Snapshot['sessions']); break
51 case 'preview': await update($, preview, () => value as Snapshot['preview']); break
52 case 'isPaneOpen': await update($, isPaneOpen, () => value as Snapshot['isPaneOpen']); break
53 case 'usage': await update($, usage, () => value as Snapshot['usage']); break
54 }
55}
56
57/** After a /clear (or at the first start), writes what this process kept back to the host. */
58let reseeding: Promise<void> | null = null
59function reseed($: EngineInterface): Promise<void> {
60 reseeding ??= (async () => {
61 try {
62 if (await read($, seeded)) return
63 for (const key of KEYS) await put($, key, kept[key])
64 await update($, seeded, () => true)
65 wasLive = true
66 } finally {
67 reseeding = null
68 }
69 })()
70 return reseeding
71}
72
73/** Changes one value from what `snapshot` reads, and keeps it here too. */
74async function write<K extends keyof Snapshot>($: EngineInterface, key: K, change: (now: Snapshot[K]) => Snapshot[K]): Promise<void> {
75 // `kept` is current once a read has seen the host live: only this module writes these values.
76 if (wasLive === null) await read($, snapshot)
77 if (!wasLive) await reseed($)
78 const next = change(kept[key])
79 kept = { ...kept, [key]: next }
80 await put($, key, next)
81}
82
83/** `fuel` is the pane's dial layout, or true for the band's; absent, the tach draws alone. */
84type View = { requestId: string; face: Face; fuel?: PaneLayout | true }
85type TankKey = keyof Tanks
86
87const TANK_KEYS = ['fiveHour', 'sevenDay', 'context'] as const
88
89const needle = new Needle()
90const fuelNeedles: Record<TankKey, Needle> = {
91 fiveHour: new Needle(SCALE, 0),
92 sevenDay: new Needle(SCALE, 0),
93 context: new Needle(SCALE, 0),
94}
95const fuelReads: Record<TankKey, number | null> = { fiveHour: null, sevenDay: null, context: null }
96let views: View[] = []
97let tick = 0
98let working = 0 // the count the needle heads for: live, or the preview
99let reads = 0 // where the needle reads this frame
100let latestUsage: RedlineUsage = EMPTY_USAGE // the last figures the engine reported
101let hasFuel = false // the fuelGauge option
102let self: number | null = null // this session's pid, once looked for
103let hasStatusLine = false
104
105const workingIn = (all: readonly RedlineSession[]) => all.filter(s => s.isWorking).length
106const tally = (all: readonly RedlineSession[]) => `${workingIn(all)} working · ${all.length} session${all.length === 1 ? '' : 's'}`
107const folder = (cwd: string) => cwd.slice(cwd.lastIndexOf('/') + 1) || cwd || '?'
108const hex = (c: number) => `#${c.toString(16).padStart(6, '0')}`
109
110/** Where each tank sits now: the stored fill, emptied once its reset time has passed. */
111function tanksNow(now: number): Tanks {
112 return {
113 fiveHour: tankFrac(latestUsage.fiveHour, now),
114 sevenDay: tankFrac(latestUsage.sevenDay, now),
115 context: fracOf(latestUsage.context),
116 }
117}
118
119/** True before any reading has been adopted (a fresh module, a hot reload, the option just on). */
120const isUnknown = (u: RedlineUsage) => u.fiveHour === null && u.sevenDay === null && u.context === null
121
122/** What the dials draw: the sprung needle positions, in 0-1, or null while a tank has no reading. */
123function drawnTanks(): Tanks {
124 const at = (key: TankKey) => (fuelReads[key] === null ? null : fuelReads[key] / SCALE)
125 return { fiveHour: at('fiveHour'), sevenDay: at('sevenDay'), context: at('context') }
126}
127
128function cells(view: View): string {
129 const reading = { needle: reads, isNapping: working <= 0 && reads < 0.3, isVenting: working >= SCALE, t: tick / FPS }
130 if (view.fuel === undefined) return gauge(view.face, reading).encode()
131 const tanks = drawnTanks()
132 return (view.fuel === true ? bandCanvas(reading, tanks) : paneCanvas(reading, tanks, view.fuel)).encode()
133}
134
135function mounted(view: View): void {
136 views = [...views.filter(v => v.requestId !== view.requestId), view]
137}
138
139async function frame($: EngineInterface): Promise<void> {
140 tick += 1
141 reads = needle.step(working, 1 / FPS)
142 if (hasFuel) {
143 const tanks = tanksNow(Date.now())
144 for (const key of TANK_KEYS) {
145 const frac = tanks[key]
146 fuelReads[key] = frac === null ? null : fuelNeedles[key].step(frac * SCALE, 1 / FPS)
147 }
148 }
149 await Promise.all(
150 views.map(async v => {
151 const res = await $.ui.blit({ requestId: v.requestId, key: 'gauge', cells: cells(v) })
152 if (res.deny !== undefined) views = views.filter(w => w !== v)
153 }),
154 )
155}
156
157function show($: EngineInterface, all: readonly RedlineSession[], previewing: number | null): void {
158 working = previewing ?? workingIn(all)
159 if (!hasStatusLine) return
160 const stage = stageFor(working).name
161 $.ui.status(previewing === null ? `${stage} · ${workingIn(all)}/${all.length}` : `${stage} · preview`)
162}
163
164/** The engine's figures for the fuel dials; null when the engine has none to give. */
165async function readUsage($: EngineInterface): Promise<RedlineUsage | null> {
166 try {
167 const { rateLimits, context } = await $.session.usage()
168 return usageFrom(rateLimits, context.percent ?? null)
169 } catch {
170 return null
171 }
172}
173
174/** Adopts new figures and writes them where the drawings can see them. */
175async function adoptUsage($: EngineInterface, found: RedlineUsage): Promise<void> {
176 latestUsage = found
177 await write($, 'usage', () => found)
178}
179
180async function refreshUsage($: EngineInterface): Promise<void> {
181 const found = await readUsage($)
182 if (found !== null) await adoptUsage($, found)
183}
184
185async function poll($: EngineInterface): Promise<void> {
186 const ps = await $.process.run(['ps', '-x', '-o', 'pid=,ppid=,tty=,args='], { timeoutMs: 5000 })
187 if (ps.exitCode !== 0) return
188 let found = sessionsIn(ps.stdout)
189 if (found.length) {
190 // lsof exits 1 when a pid has gone; the directories it did find still count.
191 const cwds = cwdsIn((await $.process.run(lsofArgv(found.map(s => s.pid)), { timeoutMs: 5000 })).stdout)
192 found = found.map(s => ({ ...s, cwd: cwds.get(s.pid) ?? '' }))
193 }
194 if (!self) self = selfIn((await $.process.run(['ps', '-x', '-o', 'pid=,ppid=,tty=,command='], { timeoutMs: 5000 })).stdout) || null
195
196 const before = await read($, snapshot)
197 if (JSON.stringify(before.sessions) === JSON.stringify(found)) return
198 await write($, 'sessions', () => found)
199 show($, found, before.preview)
200}
201
202export const register: Register = (on, options) => {
203 hasStatusLine = options.statusLine === true
204 hasFuel = options.fuelGauge === true
205
206 on('session.start', async ($, e, next) => {
207 const result = await next(e)
208 if (!hasStatusLine) $.ui.status(undefined)
209 await $.command.register({
210 name: 'redline',
211 description: `Show your Claude sessions; /redline <0-${SCALE}> previews a count, /redline live follows again`,
212 })
213 await reseed($)
214 if (hasFuel) await refreshUsage($).catch(() => {})
215 const now = await read($, snapshot)
216 show($, now.sessions, now.preview)
217 const isOpen = (await $.ui.panes()).some(p => p.id === PANE)
218 await write($, 'isPaneOpen', () => isOpen)
219
220 $.clock.every(1000 / FPS, () => void frame($).catch(() => {}))
221 if (e.isInteractive) {
222 const count = () => void poll($).catch(() => {})
223 count()
224 $.clock.every(POLL_MS, count)
225 }
226 return result
227 })
228
229 // The engine measures the session after every turn and whenever a rate-limit window moves a
230 // whole point; the figures ride the event, so the tanks only ever move on news.
231 on('session.measure', async ($, e, next) => {
232 const result = await next(e)
233 if (hasFuel) await adoptUsage($, usageFrom(e.rateLimits, e.context.percent ?? null))
234 return result
235 })
236
237 // /clear: the pane and the band stay up, so what they show is written back as soon as the
238 // host's state is the new session's (now, or from the next event if that comes later).
239 on('session.end', async ($, e, next) => {
240 const result = await next(e)
241 if (e.reason === 'clear') await reseed($).catch(() => {})
242 return result
243 })
244
245 on('command.run', { command: 'redline' }, async ($, e) => {
246 await reseed($)
247 const arg = e.args.trim()
248 if (arg === 'live') {
249 await write($, 'preview', () => null)
250 show($, (await read($, snapshot)).sessions, null)
251 return { text: 'Following your sessions again.' }
252 }
253 if (/^\d+$/.test(arg)) {
254 const count = Math.min(Number(arg), SCALE)
255 await write($, 'preview', () => count)
256 show($, (await read($, snapshot)).sessions, count)
257 return { text: `Previewing ${count} working: ${stageFor(count).name}. /redline live to follow your sessions again.` }
258 }
259 if (arg !== '') return { text: `Usage: /redline, /redline <0-${SCALE}>, /redline live` }
260
261 if ((await read($, snapshot)).isPaneOpen) {
262 await $.ui.close({ id: PANE })
263 return { text: 'Redline closed.' }
264 }
265 const rows = LARGE.rows + 12 + (hasFuel ? TALL.rows - LARGE.rows : 0)
266 const opened = await $.ui.open({ id: PANE, title: 'Redline', rows })
267 await write($, 'isPaneOpen', () => true)
268 return { text: opened.isPlaced ? 'Redline opened.' : 'Redline opened; widen the terminal to see it.' }
269 })
270
271 on('ui.close', async ($, e, next) => {
272 const result = await next(e)
273 if (e.id === PANE) await write($, 'isPaneOpen', () => false)
274 return result
275 })
276
277 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
278 // Drawn from what this process kept after a /clear; the host gets it back off the render.
279 if (!(await read($, seeded))) $.clock.after(0, () => void reseed($).catch(() => {}))
280 const { sessions: all, preview: previewing, isPaneOpen: isOpen, usage: heldUsage } = await read($, snapshot)
281 if (hasFuel && isUnknown(latestUsage)) latestUsage = heldUsage
282 if (e.props.hasSurvey || isOpen) {
283 views = views.filter(v => v.requestId !== e.requestId)
284 return next(e)
285 }
286 const shown = previewing ?? workingIn(all)
287 const stage = stageFor(shown)
288 const count = previewing === null ? tally(all) : 'preview · /redline live'
289
290 if (e.surface !== 'terminal') {
291 const { Text } = $.ui.resolve(e)
292 return <Text color={hex(colorFor(shown))}>{`${stage.name} · ${count}`}</Text>
293 }
294 const { Box, Text, Raster } = $.ui.resolve(e)
295 const withFuel = hasFuel && e.props.bodyColumns >= BAND_COLS + BAND_WORDS
296 const view: View = { requestId: e.requestId, face: SMALL, fuel: withFuel ? true : undefined }
297 mounted(view)
298 return (
299 <Box flexDirection="row">
300 <Raster key="gauge" columns={withFuel ? BAND_COLS : SMALL.cols} rows={SMALL.rows} cells={cells(view)} />
301 <Box flexDirection="column" paddingX={1}>
302 <Text bold color={hex(colorFor(shown))}>{stage.name}</Text>
303 {e.props.bodyColumns >= (withFuel ? BAND_COLS : SMALL.cols) + 30 && <Text dimColor wrap="truncate">{stage.line}</Text>}
304 <Text dimColor wrap="truncate">{count}</Text>
305 </Box>
306 </Box>
307 )
308 })
309
310 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
311 if (!(await read($, seeded))) $.clock.after(0, () => void reseed($).catch(() => {}))
312 const { sessions: all, preview: previewing, usage: heldUsage } = await read($, snapshot)
313 if (hasFuel && isUnknown(latestUsage)) latestUsage = heldUsage
314 const shown = previewing ?? workingIn(all)
315 const stage = stageFor(shown)
316 const { Box, Text } = $.ui.resolve(e)
317 const list = (
318 <Box flexDirection="column" paddingX={1}>
319 {all.length === 0 && <Text dimColor>No Claude sessions in terminals.</Text>}
320 {all.map(s => (
321 <Text wrap="truncate" dimColor={!s.isWorking}>
322 {`${s.isWorking ? '●' : '○'} ${s.tty.padEnd(8)} ${folder(s.cwd)}${s.pid === self ? ' (this one)' : ''}`}
323 </Text>
324 ))}
325 </Box>
326 )
327 const words = (
328 <Box flexDirection="column" paddingX={1}>
329 <Text bold color={hex(colorFor(shown))}>{previewing === null ? stage.name : `${stage.name} (preview)`}</Text>
330 <Text dimColor wrap="wrap">{stage.line}</Text>
331 <Text dimColor>{tally(all)}</Text>
332 </Box>
333 )
334 if (e.surface !== 'terminal') {
335 return <Box flexDirection="column">{words}{list}</Box>
336 }
337 const { Raster } = $.ui.resolve(e)
338 // A narrow pane stacks the dials, as many to a row as fit; a resize renders again and remounts.
339 const fuel = hasFuel && e.props.bodyColumns >= BIG.cols ? paneLayout(e.props.bodyColumns) : undefined
340 const view: View = { requestId: e.requestId, face: LARGE, fuel }
341 mounted(view)
342 const rasterCols = fuel?.cols ?? LARGE.cols
343 return (
344 <Box flexDirection="column" paddingTop={1}>
345 <Box flexDirection={e.props.bodyColumns >= rasterCols + 24 ? 'row' : 'column'}>
346 <Raster key="gauge" columns={rasterCols} rows={fuel?.rows ?? LARGE.rows} cells={cells(view)} />
347 {words}
348 </Box>
349 <Text> </Text>
350 {list}
351 </Box>
352 )
353 })
354}
355hooks/fuel.ts 144 lines1// The fuel dials: a shallow arc with F at full (left) and E at empty (right), a red reserve at
2// the E end. Same dial library and palette as the tach. Each dial reads how much of its tank is
3// used: the plan's five-hour or seven-day rate-limit window, or this session's context.
4
5import type { SessionRateLimit } from 'claude-code'
6
7import type { RedlineTank, RedlineUsage } from '../types'
8import type { Canvas } from './canvas'
9import { dial, shallow } from './dial'
10import { Dots } from './dots'
11import { DIAL, DIAL_DIM, HUB, LARGE, NEEDLE, RED_C, SMALL, TICK, tachDots, type Reading } from './gauge'
12
13/** How much of each tank is used, 0 to 1; null while that tank has no reading. */
14export type Tanks = { fiveHour: number | null; sevenDay: number | null; context: number | null }
15
16export type FuelFace = { cols: number; rows: number; big: boolean }
17
18/** Beside the tach in the band: 4 rows, a label but no F/E letters. */
19export const MINI: FuelFace = { cols: 11, rows: 4, big: false }
20/** Under the tach in the pane: 6 rows, labelled F and E. */
21export const BIG: FuelFace = { cols: 15, rows: 6, big: true }
22
23const GAP = 2
24export const BAND_COLS = SMALL.cols + GAP + 3 * MINI.cols + 2 * GAP
25
26// The tach's hub is left of its panel's middle, its steam lane to the right; centered on the hub it spans this.
27const TACH_SPAN = 2 * (LARGE.cols - LARGE.cx / 2)
28
29/** How the pane fits the three big dials into `columns`: as many to a row as fit, 1 to 3. */
30export type PaneLayout = { perRow: number; cols: number; rows: number }
31
32export function paneLayout(columns: number): PaneLayout {
33 const perRow = Math.max(1, Math.min(3, Math.floor((columns + GAP) / (BIG.cols + GAP))))
34 const lines = Math.ceil(3 / perRow)
35 const rowCols = perRow * BIG.cols + (perRow - 1) * GAP
36 return {
37 perRow,
38 // Wide enough, where there is room, for the tach's hub to sit over the middle of the dials.
39 cols: Math.max(LARGE.cols, rowCols, Math.min(columns, TACH_SPAN)),
40 rows: LARGE.rows + lines * (BIG.rows + 1) + 1, // a blank row under the last dials, as above each
41 }
42}
43
44/** The pane at its widest: the three dials in one row under the tach. */
45export const WIDE = paneLayout(Infinity)
46/** The tallest the pane's dash gets: one dial to a row. */
47export const TALL = paneLayout(0)
48
49const FROM = (150 / 180) * Math.PI
50const SWEEP = (120 / 180) * Math.PI
51const RESERVE = 0.85 // the red band starts here, as a fraction of the tank used
52
53/**
54 * `fills` is for a tank that fills as it is used, the context: it reads E at the left and F at the
55 * red end. The plan windows drain, F at the left and E at the red end. Either way the needle
56 * swings right, into the red, as the tank is used.
57 */
58export function fuelDial(face: FuelFace, frac: number | null, label: string, fills = false): Dots {
59 const dots = new Dots(face.cols, face.rows)
60 const dim = frac === null
61 const reserve = (t: number) => t >= RESERVE
62 dial(dots, {
63 dial: shallow(face.cols, face.rows, FROM, SWEEP),
64 arc: {
65 colorAt: t => (dim ? DIAL_DIM : reserve(t) ? RED_C : DIAL),
66 widthAt: t => (!dim && reserve(t) ? 1 : 0),
67 },
68 ticks: {
69 count: face.big ? 8 : 6,
70 majorEvery: face.big ? 4 : 3,
71 length: 2,
72 majorLength: 3,
73 rank: 2,
74 colorAt: t => (dim ? DIAL_DIM : reserve(t) ? RED_C : TICK),
75 },
76 needle: dim
77 ? undefined
78 : {
79 t: frac,
80 color: reserve(frac) ? RED_C : NEEDLE,
81 inner: face.big ? 4 : 2.5,
82 tail: face.big ? 2.5 : 0,
83 width: face.big ? 1.4 : 1.0,
84 hub: { color: HUB },
85 },
86 })
87
88 dots.text(0, Math.round(face.cols / 2 - label.length / 2), label, dim ? DIAL_DIM : TICK)
89 if (face.big) {
90 const [start, end] = fills ? ['E', 'F'] : ['F', 'E']
91 dots.text(face.rows - 2, 0, start, dim ? DIAL_DIM : TICK)
92 dots.text(face.rows - 2, face.cols - 1, end, dim ? DIAL_DIM : RED_C)
93 }
94 return dots
95}
96
97const LABELS = ['5H', '7D', 'CTX'] as const
98
99const dialsFor = (face: FuelFace, tanks: Tanks): Dots[] =>
100 [tanks.fiveHour, tanks.sevenDay, tanks.context].map((frac, i) => fuelDial(face, frac, LABELS[i]!, i === 2))
101
102/** The band's dash: the tach with the three small dials beside it. */
103export function bandCanvas(reading: Reading, tanks: Tanks): Canvas {
104 const out = new Dots(BAND_COLS, SMALL.rows)
105 out.blit(tachDots(SMALL, reading), 0, 0)
106 dialsFor(MINI, tanks).forEach((panel, i) => out.blit(panel, 0, SMALL.cols + GAP + i * (MINI.cols + GAP)))
107 return out.toCanvas()
108}
109
110/** The pane's dash: the tach with the three big dials under it, `layout.perRow` to a row, each row centered. */
111export function paneCanvas(reading: Reading, tanks: Tanks, layout: PaneLayout = WIDE): Canvas {
112 const out = new Dots(layout.cols, layout.rows)
113 out.blit(tachDots(LARGE, reading), 0, Math.max(0, Math.min(layout.cols - LARGE.cols, Math.round(layout.cols / 2 - LARGE.cx / 2))))
114 const dials = dialsFor(BIG, tanks)
115 dials.forEach((panel, i) => {
116 const line = Math.floor(i / layout.perRow)
117 const inLine = Math.min(layout.perRow, dials.length - line * layout.perRow)
118 const left = Math.floor((layout.cols - (inLine * BIG.cols + (inLine - 1) * GAP)) / 2)
119 out.blit(panel, LARGE.rows + 1 + line * (BIG.rows + 1), left + (i % layout.perRow) * (BIG.cols + GAP))
120 })
121 return out.toCanvas()
122}
123
124/** A tank's fill from its last reading: emptied once its reset time has passed. */
125export function tankFrac(tank: RedlineTank | null, now: number): number | null {
126 if (tank === null) return null
127 if (tank.resetsAt !== null && now >= tank.resetsAt) return 0
128 return Math.max(0, Math.min(1, tank.percent / 100))
129}
130
131/** A bare percentage, clamped, as a fill; null stays null. */
132export const fracOf = (percent: number | null): number | null => (percent === null ? null : Math.max(0, Math.min(1, percent / 100)))
133
134/** The two plan windows and the context, from what a measurement reported. */
135export function usageFrom(rateLimits: readonly SessionRateLimit[], contextPercent: number | null): RedlineUsage {
136 const tank = (kind: string): RedlineTank | null => {
137 const limit = rateLimits.find(l => l.kind === kind)
138 if (!limit) return null
139 const parsed = limit.resetsAt === undefined ? NaN : Date.parse(limit.resetsAt)
140 return { percent: limit.percentUsed, resetsAt: Number.isFinite(parsed) ? parsed : null }
141 }
142 return { fiveHour: tank('five_hour'), sevenDay: tank('seven_day'), context: contextPercent }
143}
144hooks/gauge.ts 148 lines1// A turbo boost gauge drawn in braille: the needle reads how many Claude sessions are working.
2// The dial wears a dim housing, the redline zone is painted on thicker, and the needle tapers
3// from a ringed hub. Built from the dial library; the fuel dials in `fuel.ts` use it too.
4
5import type { Canvas } from './canvas'
6import { arc, at, dial } from './dial'
7import { Dots } from './dots'
8
9export const SCALE = 25 // the needle pins here
10const AMBER = 13
11const RED = 18
12
13export const DIAL = 0x6c6c6c
14export const DIAL_DIM = 0x4a4a4a
15export const TICK = 0xd0d0d0
16export const AMBER_C = 0xffaf00
17export const RED_C = 0xff3b30
18export const NEEDLE = 0xff875f
19export const HUB = 0xeeeeee
20const STEAM = 0xc7c7cc
21const SNORE = 0x9aa5b1
22
23export type Stage = { from: number; name: string; line: string }
24
25/** Each stage starts at `from` working sessions. */
26export const STAGES: readonly Stage[] = [
27 { from: 0, name: 'Napping', line: 'Engine off.' },
28 { from: 1, name: 'Idling', line: 'This is peaceful.' },
29 { from: 3, name: 'Cruising', line: 'Smooth and steady.' },
30 { from: 6, name: 'Busy', line: "Things are heating up." },
31 { from: 9, name: 'Very Busy', line: "Needle's climbing." },
32 { from: AMBER, name: 'Under Pressure', line: 'Pushing hard.' },
33 { from: RED, name: 'Redline', line: 'Needle in the red.' },
34 { from: 21, name: 'Overboost', line: "Past the red. Something's going to give." },
35 { from: SCALE, name: 'Blown', line: 'Pinned. Blow-off valve wide open.' },
36]
37
38export const stageFor = (working: number): Stage => STAGES.findLast(s => working >= s.from) ?? STAGES[0]!
39
40export const colorFor = (working: number): number =>
41 working <= 0 ? SNORE : working >= RED ? RED_C : working >= AMBER ? AMBER_C : NEEDLE
42
43const zoneColor = (value: number) => (value >= RED ? RED_C : value >= AMBER ? AMBER_C : TICK)
44
45/**
46 * A damped spring: it swings to a new count with a little overshoot, trembles more the higher it
47 * reads, and past the top presses against the pin and rattles there. `scale` is where the pin
48 * sits and `jitter` how much it trembles; the fuel needles take the same spring without jitter.
49 */
50export class Needle {
51 value = 0
52 private velocity = 0
53
54 constructor(
55 private readonly scale = SCALE,
56 private readonly jitter = 1,
57 ) {}
58
59 /** Moves `dt` seconds toward `target`; returns where the needle reads now. */
60 step(target: number, dt: number): number {
61 const goal = target >= this.scale ? this.scale + 2 : Math.min(target, this.scale)
62 this.velocity += (18 * (goal - this.value) - 5 * this.velocity) * dt
63 this.value += this.velocity * dt
64 if (this.value > this.scale) {
65 this.value = this.scale
66 this.velocity = -Math.abs(this.velocity) * 0.6
67 }
68 if (this.value < 0) {
69 this.value = 0
70 this.velocity = Math.abs(this.velocity) * 0.3
71 }
72 const tremble = (Math.max(0, Math.min(target, this.scale) - 6) / (this.scale - 6)) * 0.5 * this.jitter
73 return Math.max(0, Math.min(this.scale, this.value + (Math.random() * 2 - 1) * tremble))
74 }
75}
76
77/** A dial in dots: center, radius, and the arc the scale sweeps clockwise from `from`. */
78export type Face = {
79 cols: number
80 rows: number
81 cx: number
82 cy: number
83 r: number
84 from: number
85 sweep: number
86 numbers: boolean
87 lane: number // columns on the right for the snore and the steam
88}
89
90/** Above the prompt: 240°, so at rest the needle points down-left. */
91export const SMALL: Face = { cols: 11 + 5, rows: 4, cx: 11, cy: 9, r: 9, from: (7 / 6) * Math.PI, sweep: (4 / 3) * Math.PI, numbers: false, lane: 5 }
92/** In the pane: 270°, numbered. */
93export const LARGE: Face = { cols: 24 + 6, rows: 11, cx: 24, cy: 22, r: 21, from: 1.25 * Math.PI, sweep: 1.5 * Math.PI, numbers: true, lane: 6 }
94
95export type Reading = { needle: number; isNapping: boolean; isVenting: boolean; t: number }
96
97/** The tach alone, as dots, for composing with the fuel dials. */
98export function tachDots(face: Face, reading: Reading): Dots {
99 const dots = new Dots(face.cols, face.rows)
100 const zone = (t: number) => zoneColor(t * SCALE)
101
102 if (face.numbers) arc(dots, face, { colorAt: () => DIAL_DIM, radius: face.r + 2, rank: 0 })
103 dial(dots, {
104 dial: face,
105 arc: { colorAt: zone, widthAt: t => (t * SCALE >= RED ? 1 : 0) },
106 ticks: face.numbers
107 ? { count: SCALE, majorEvery: 5, length: 2, majorLength: 5, rank: 2, colorAt: zone }
108 : { count: 5, majorEvery: 1, length: 3, majorLength: 3, colorAt: zone },
109 needle: {
110 t: reading.needle / SCALE,
111 color: reading.needle >= RED ? RED_C : NEEDLE,
112 inner: face.numbers ? 5 : 3,
113 tail: face.numbers ? 3 : 1.5,
114 width: face.numbers ? 1.6 : 1.1,
115 hub: face.numbers ? { color: HUB, ringRadius: 3, ringColor: DIAL } : { color: HUB },
116 },
117 })
118
119 if (face.numbers) {
120 for (const v of [0, 5, 10, 15, 20, 25]) {
121 const [x, y] = at(face, v / SCALE, face.r - 9)
122 const s = String(v)
123 dots.text(Math.round(y / 4), Math.round(x / 2 - (s.length - 1) / 2), s, zoneColor(v))
124 }
125 dots.text(face.rows - 1, Math.round(face.cx / 2 - 7 / 2), 'CLAUDES', DIAL)
126 }
127
128 const lane = face.cols - face.lane
129 if (reading.isNapping) {
130 const k = Math.floor(reading.t * 2) % 8
131 const snore = ['z', 'z', 'Z', 'Z']
132 for (let i = 0; i < Math.min(k, 4); i++) dots.text(face.rows - 1 - i, lane + 1 + i, snore[i]!, SNORE)
133 }
134 if (reading.isVenting) {
135 for (let i = 0; i < 3; i++) {
136 const u = (reading.t * 1.7 + i / 3) % 1
137 const row = Math.round((face.rows - 1) * (1 - u))
138 const col = lane + 1 + Math.round(u * (face.lane - 2))
139 dots.text(row, col, u < 0.4 ? '▓' : u < 0.7 ? '▒' : '░', STEAM)
140 }
141 }
142 return dots
143}
144
145export function gauge(face: Face, reading: Reading): Canvas {
146 return tachDots(face, reading).toCanvas()
147}
148hooks/sessions.ts 72 lines1// The Claude sessions running in your terminals, and which of them are mid-turn.
2
3export type Session = { pid: number; tty: string; isWorking: boolean; cwd: string }
4
5/** Working directories of `pids`, in lsof's field format: `p<pid>`, then `n<path>`. */
6export const lsofArgv = (pids: readonly number[]) => ['lsof', '-a', '-d', 'cwd', '-Fn', '-p', pids.join(',')]
7
8type Proc = { pid: number; ppid: number; tty: string; cmd: string; arg1: string }
9
10const base = (path: string) => path.slice(path.lastIndexOf('/') + 1)
11
12function procsIn(ps: string): Proc[] {
13 return ps
14 .split('\n')
15 .map(line => {
16 const [pid = '', ppid = '', tty = '', argv0 = '', arg1 = ''] = line.trim().split(/\s+/)
17 return { pid: Number(pid), ppid: Number(ppid), tty, cmd: base(argv0), arg1 }
18 })
19 .filter(p => p.pid > 0 && p.cmd !== '')
20}
21
22/** `claude` with a terminal, and not one of the daemon's helpers. */
23const isSession = (p: Proc) =>
24 p.cmd === 'claude' && p.tty !== '??' && p.tty !== '?' && p.arg1 !== 'daemon' && !p.arg1.startsWith('bg-')
25
26/**
27 * The sessions in `ps` output, ordered by terminal. A session is working while it has a
28 * `caffeinate` child, which Claude Code holds on macOS through a turn.
29 */
30export function sessionsIn(ps: string): Session[] {
31 const procs = procsIn(ps)
32 const caffeinated = new Set(procs.filter(p => p.cmd === 'caffeinate').map(p => p.ppid))
33 return procs
34 .filter(isSession)
35 .map(p => ({ pid: p.pid, tty: p.tty, isWorking: caffeinated.has(p.pid), cwd: '' }))
36 .sort((a, b) => a.tty.localeCompare(b.tty) || a.pid - b.pid)
37}
38
39/** The session `pid` runs under: itself or its nearest session ancestor; 0 for none. */
40/**
41 * This session, from the table `SELF_ARGV` printed: the session its own row runs under. Every
42 * redline polls with the same `ps` at the same moment, so the lookup asks with a format of its own,
43 * and only another session looking itself up in the same instant makes it ambiguous: then 0, and
44 * the next poll asks again.
45 */
46export const SELF_ARGV = ['ps', '-x', '-o', 'pid=,ppid=,tty=,command='] as const
47
48export function selfIn(ps: string): number {
49 const signature = SELF_ARGV.join(' ')
50 const ours = ps.split('\n').filter(line => line.trimEnd().endsWith(signature)).map(line => Number(line.trim().split(/\s+/)[0]))
51 const found = new Set(ours.map(pid => sessionOf(ps, pid)).filter(pid => pid > 0))
52 return found.size === 1 ? [...found][0]! : 0
53}
54
55export function sessionOf(ps: string, pid: number): number {
56 const procs = new Map(procsIn(ps).map(p => [p.pid, p]))
57 for (let p = procs.get(pid), hops = 0; p && hops < 64; p = procs.get(p.ppid), hops++) {
58 if (isSession(p)) return p.pid
59 }
60 return 0
61}
62
63export function cwdsIn(lsof: string): Map<number, string> {
64 const cwds = new Map<number, string>()
65 let pid = 0
66 for (const line of lsof.split('\n')) {
67 if (line.startsWith('p')) pid = Number(line.slice(1))
68 else if (line.startsWith('n') && pid) cwds.set(pid, line.slice(1))
69 }
70 return cwds
71}
72hooks/canvas.ts 37 lines1// A grid of terminal cells, encoded as a `Raster` takes them: row-major little-endian u32
2// triplets [codePoint, foreground, background], base64.
3
4const DEFAULT = 0x01000000 // the terminal's own color
5
6export class Canvas {
7 readonly cells: Uint32Array
8
9 constructor(
10 readonly rows: number,
11 readonly cols: number,
12 ) {
13 this.cells = new Uint32Array(rows * cols * 3)
14 for (let i = 0; i < rows * cols; i++) this.cells.set([0x20, DEFAULT, DEFAULT], i * 3)
15 }
16
17 /** Writes `s` from (r, c) in color `fg`; characters off the grid are dropped. */
18 put(r: number, c: number, s: string, fg: number): void {
19 if (r < 0 || r >= this.rows) return
20 let x = c
21 for (const ch of s) {
22 if (x >= 0 && x < this.cols) this.cells.set([ch.codePointAt(0)!, fg, DEFAULT], (r * this.cols + x) * 3)
23 x++
24 }
25 }
26
27 line(r: number): string {
28 let s = ''
29 for (let c = 0; c < this.cols; c++) s += String.fromCodePoint(this.cells[(r * this.cols + c) * 3] ?? 0x20)
30 return s
31 }
32
33 encode(): string {
34 return new Uint8Array(this.cells.buffer).toBase64()
35 }
36}
37hooks/dial.ts 123 lines1// The dial standard library: the parts every gauge in redline is built from. A dial is a pivot,
2// a radius, and the arc its scale sweeps clockwise from `from`; callers work in `t`, 0 at the
3// start of the sweep and 1 at its end, and supply the colors. The braille grid is in `dots.ts`.
4
5import { Dots } from './dots'
6
7/** A dial's scale: where the pivot sits and the arc it sweeps. */
8export type Dial = { cx: number; cy: number; r: number; from: number; sweep: number }
9
10/** A shallow dial whose pivot sits at the bottom edge of a `cols` × `rows` panel. */
11export function shallow(cols: number, rows: number, from: number, sweep: number): Dial {
12 const cx = cols - 0.5 // the panel's middle, between two dot columns, so the dial rounds the same both sides
13 const cy = rows * 4 - 2
14 const reach = Math.max(Math.abs(Math.cos(from)), Math.abs(Math.cos(from - sweep)))
15 const r = Math.floor(Math.min((cols - 2) / reach, cy - 5))
16 return { cx, cy, r, from, sweep }
17}
18
19export const angleAt = (d: Dial, t: number) => d.from - t * d.sweep
20
21/** The point `r` from the pivot at angle `a`. */
22export const polar = (d: Dial, a: number, r: number): [number, number] => [d.cx + r * Math.cos(a), d.cy - r * Math.sin(a)]
23
24/** The point at `t` on a circle of radius `r`. */
25export const at = (d: Dial, t: number, r = d.r): [number, number] => polar(d, angleAt(d, t), r)
26
27export type ArcSpec = {
28 colorAt: (t: number) => number
29 /** Extra dots drawn outward at `t`, for a painted zone. */
30 widthAt?: (t: number) => number
31 radius?: number
32 rank?: number
33}
34
35/** The scale arc, in fine steps so it reads as a line. */
36export function arc(dots: Dots, d: Dial, spec: ArcSpec): void {
37 const r = spec.radius ?? d.r
38 const rank = spec.rank ?? 1
39 const widthAt = spec.widthAt ?? (() => 0)
40 const n = Math.ceil(d.sweep * r * 1.6)
41 for (let i = 0; i <= n; i++) {
42 const t = i / n
43 const color = spec.colorAt(t)
44 const [x, y] = at(d, t, r)
45 dots.dot(x, y, color, rank)
46 for (let k = 1, w = widthAt(t); k <= w; k++) {
47 const [wx, wy] = at(d, t, r + k)
48 dots.dot(wx, wy, color, rank)
49 }
50 }
51}
52
53export type TickSpec = {
54 count: number
55 colorAt: (t: number) => number
56 length?: number
57 majorLength?: number
58 /** Every `majorEvery`-th tick is a major one, one rank above the rest; 0 (the default) makes none. */
59 majorEvery?: number
60 rank?: number
61 /** How far inside the arc the ticks start. */
62 inner?: number
63}
64
65/** The ticks along the scale. */
66export function ticks(dots: Dots, d: Dial, spec: TickSpec): void {
67 const length = spec.length ?? 2
68 const majorLength = spec.majorLength ?? length + 1
69 const rank = spec.rank ?? 2
70 const inner = spec.inner ?? 1
71 for (let k = 0; k <= spec.count; k++) {
72 const t = k / spec.count
73 const major = spec.majorEvery ? k % spec.majorEvery === 0 : false
74 const [x0, y0] = at(d, t, d.r - inner)
75 const [x1, y1] = at(d, t, d.r - (major ? majorLength : length))
76 dots.line(x0, y0, x1, y1, spec.colorAt(t), major ? rank + 1 : rank)
77 }
78}
79
80export type NeedleSpec = {
81 t: number
82 color: number
83 /** The tip stops this far inside the arc. */
84 inner?: number
85 /** How far past the pivot the tail runs. */
86 tail?: number
87 /** The needle's width at the pivot; it tapers to the tip. */
88 width?: number
89 hub?: { color: number; ringRadius?: number; ringColor?: number }
90}
91
92/** The needle at `t`, with its tail and hub. */
93export function needle(dots: Dots, d: Dial, spec: NeedleSpec): void {
94 const a = angleAt(d, spec.t)
95 const [tipX, tipY] = polar(d, a, d.r - (spec.inner ?? 4))
96 const [tailX, tailY] = polar(d, a + Math.PI, spec.tail ?? 2)
97 dots.needle(d.cx, d.cy, tipX, tipY, spec.width ?? 1.2, spec.color, 3)
98 dots.line(tailX, tailY, tipX, tipY, spec.color, 3)
99 if (spec.hub) {
100 if (spec.hub.ringRadius) {
101 for (let i = 0; i < 18; i++) {
102 const ha = (i / 18) * 2 * Math.PI
103 dots.dot(d.cx + spec.hub.ringRadius * Math.cos(ha), d.cy - spec.hub.ringRadius * Math.sin(ha), spec.hub.ringColor ?? spec.hub.color, 4)
104 }
105 }
106 for (const [dx, dy] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]] as const) dots.dot(d.cx + dx, d.cy + dy, spec.hub.color, 5)
107 }
108}
109
110export type DialSpec = {
111 dial: Dial
112 arc?: ArcSpec
113 ticks?: TickSpec
114 needle?: NeedleSpec
115}
116
117/** Draws a dial from its parts, the needle last. */
118export function dial(dots: Dots, spec: DialSpec): void {
119 if (spec.arc) arc(dots, spec.dial, spec.arc)
120 if (spec.ticks) ticks(dots, spec.dial, spec.ticks)
121 if (spec.needle) needle(dots, spec.dial, spec.needle)
122}
123hooks/dots.ts 105 lines1// The braille dot grid the gauges draw into: each terminal cell is a 2 × 4 grid of dots, so the
2// arcs stay round and the needles thin. A cell takes the color of the highest-ranked dot drawn in
3// it; text cells sit above every dot.
4
5import { Canvas } from './canvas'
6
7const BIT = [[0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80]] as const // braille dot bits by [row][column]
8
9export class Dots {
10 readonly bits: Uint8Array
11 readonly color: Int32Array
12 readonly rank: Int8Array
13 private readonly chars = new Map<number, string>()
14
15 constructor(readonly cols: number, readonly rows: number) {
16 this.bits = new Uint8Array(cols * rows)
17 this.color = new Int32Array(cols * rows)
18 this.rank = new Int8Array(cols * rows).fill(-1)
19 }
20
21 dot(x: number, y: number, color: number, rank: number): void {
22 const [px, py] = [Math.round(x), Math.round(y)]
23 const [c, r] = [Math.floor(px / 2), Math.floor(py / 4)]
24 if (px < 0 || py < 0 || c >= this.cols || r >= this.rows) return
25 const i = r * this.cols + c
26 this.bits[i]! |= BIT[py % 4]![px % 2]!
27 if (rank >= this.rank[i]!) {
28 this.rank[i] = rank
29 this.color[i] = color
30 }
31 }
32
33 line(x0: number, y0: number, x1: number, y1: number, color: number, rank: number): void {
34 const n = Math.ceil(Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0)) * 1.5) || 1
35 for (let i = 0; i <= n; i++) this.dot(x0 + ((x1 - x0) * i) / n, y0 + ((y1 - y0) * i) / n, color, rank)
36 }
37
38 /** A needle from the hub to the tip, `wBase` dots wide at the hub and a point at the tip. */
39 needle(hubX: number, hubY: number, tipX: number, tipY: number, wBase: number, color: number, rank: number): void {
40 const dx = tipX - hubX
41 const dy = tipY - hubY
42 const len = Math.hypot(dx, dy) || 1
43 const px = -dy / len
44 const py = dx / len
45 const n = Math.ceil(len * 2)
46 for (let i = 0; i <= n; i++) {
47 const t = i / n
48 const w = wBase * (1 - t)
49 const x = hubX + dx * t
50 const y = hubY + dy * t
51 for (let o = -w; o <= w; o += 0.5) this.dot(x + px * o, y + py * o, color, rank)
52 }
53 }
54
55 /** Writes `s` as text cells from (r, c); characters off the grid are dropped. */
56 text(r: number, c: number, s: string, color: number): void {
57 if (r < 0 || r >= this.rows) return
58 let x = c
59 for (const ch of s) {
60 if (x >= 0 && x < this.cols) {
61 const i = r * this.cols + x
62 this.bits[i] = 0
63 this.rank[i] = 99
64 this.color[i] = color
65 this.chars.set(i, ch)
66 }
67 x++
68 }
69 }
70
71 /** Pastes `other` at (r, c): dots merge by rank, text stays text. */
72 blit(other: Dots, r: number, c: number): void {
73 for (let i = 0; i < other.bits.length; i++) {
74 const or = Math.floor(i / other.cols) + r
75 const oc = (i % other.cols) + c
76 if (or < 0 || or >= this.rows || oc < 0 || oc >= this.cols) continue
77 const to = or * this.cols + oc
78 const ch = other.chars.get(i)
79 if (ch !== undefined) {
80 this.bits[to] = 0
81 this.rank[to] = 99
82 this.color[to] = other.color[i]!
83 this.chars.set(to, ch)
84 continue
85 }
86 if (!other.bits[i]) continue
87 this.bits[to]! |= other.bits[i]!
88 if (other.rank[i]! >= this.rank[to]!) {
89 this.rank[to] = other.rank[i]!
90 this.color[to] = other.color[i]!
91 }
92 }
93 }
94
95 toCanvas(): Canvas {
96 const cv = new Canvas(this.rows, this.cols)
97 for (let i = 0; i < this.bits.length; i++) {
98 const ch = this.chars.get(i)
99 if (ch !== undefined) cv.put(Math.floor(i / this.cols), i % this.cols, ch, this.color[i]!)
100 else if (this.bits[i]) cv.put(Math.floor(i / this.cols), i % this.cols, String.fromCharCode(0x2800 + this.bits[i]!), this.color[i]!)
101 }
102 return cv
103 }
104}
105types/index.d.ts 28 lines1export type RedlineSession = { pid: number; tty: string; isWorking: boolean; cwd: string }
2
3/** One rate-limit window's last reading: how much is used, and when it refills. */
4export type RedlineTank = { percent: number; resetsAt: number | null }
5
6/** The figures behind the fuel dials; each null until the engine has a reading. */
7export type RedlineUsage = {
8 fiveHour: RedlineTank | null
9 sevenDay: RedlineTank | null
10 /** This session's context fill, percent used; null until a response reports one. */
11 context: number | null
12}
13
14declare module 'claude-code' {
15 interface PluginState {
16 redline: {
17 sessions: RedlineSession[]
18 /** A count shown instead of the live one (`/redline 12`); null follows the sessions. */
19 preview: number | null
20 isPaneOpen: boolean
21 /** True once this session's values are written; false after a /clear empties them. */
22 seeded: boolean
23 /** The plan windows and the context, for the fuel dials. */
24 usage: RedlineUsage
25 }
26 }
27}
28