A SYS band above the prompt: CPU load, RAM in use and battery, each with a 6-cell bar and traffic-light colours, read every 5 seconds; a btop button that opens…

A ▣ SYS row above the prompt: how busy the machine is, read every 5 seconds.
▣ SYS CPU 6% ▰▱▱▱▱▱ · RAM 28.9/64G ▰▰▰▱▱▱ · ⚡ 65% [ btop ]
sysctl -n vm.loadavg over sysctl -n hw.ncpu, truncated). It can pass 100% when more is runnable than there are cores; the bar stops at six cells.vm_stat, times the page size in its header, over sysctl -n hw.memsize, in GiB.pmset -g batt. A machine with no battery has no ⚡ segment.The figures are the same arithmetic as herdr-setup/monitor/herdr_monitor.py (sysinfo_parts, _mem_used_bytes).
A $.clock timer reads every 5 seconds, never on render. Each command runs through $.process.run with a 2-second limit; one that fails leaves only its part out. The reading is kept in $.state under sys.reading. The timer runs only while a terminal shows the session ($.session.surfaces()) and the row is on; it stops otherwise, and /sys on or a new session starts it again.
The btop button and /sys btop open btop in a herdr pane below this one:
herdr pane split --current --direction down --no-focus --cwd <session cwd>herdr pane run <new pane id> "XDG_CONFIG_HOME=$HOME/.config/btop-mini btop" (the new pane's shell expands $HOME, so btop reads the compact config in ~/.config/btop-mini).Outside herdr (HERDR_ENV is not 1) nothing runs and the answer is "btop needs a herdr pane". While one open is under way, another press or /sys btop splits nothing and answers "btop is already opening". The button works only while the band has focus (ctrl+x tab, or a click in Claude Code's fullscreen renderer; the classic renderer reports no clicks), so it has a command too:
| Button | Command |
|---|---|
btop | /sys btop |
/sys on | off | now | btop
on, off: show or hide the row and start or stop the reader; kept in the mod's store and winning over the band option.now (also /sys alone): read at once and answer the figures, for example CPU 6% · RAM 19.8/64G (31%) · ⚡ 64% (not charging).btop: as the button.| Option | Values | Default |
|---|---|---|
band | on, off | on |
From the local marketplace folder (~/.claude/mods):
claude plugin install sys@zehui-mods --scope user
sysctl, vm_stat, pmset and, for btop, herdr, and nothing else. No model is called.zm-10-sys, it takes its slot in the zehui-mods stack order (SYS 10, PLAN 20, HANDOFF 30, CACHE 40, AGENTS 50, GOAL 60, HERE 70, NEXT 80); other bands follow. It yields to a survey. Terminal only. English only.hooks/register.tsx 210 lines1// The sys mod: the ▣ SYS row (row.tsx draws it), the reader behind it, the
2// btop button and /sys. The reader runs every 5 seconds from a $.clock timer
3// (never on render) while a terminal shows the session and the row is on;
4// each command it runs (sysctl, vm_stat, pmset) has a 2-second limit, and the
5// reading is kept in $.state under sys.reading.
6import { atom, read, update } from 'claude-code'
7import type { EngineInterface, PluginOptions, ProcessRunResult, Register, Timer } from 'claude-code'
8
9import type { SysReading } from '../types'
10import {
11 COMMAND,
12 HERDR_TIMEOUT_MS,
13 NEEDS_HERDR,
14 RUN_TIMEOUT_MS,
15 STORE_BAND,
16 TICK_MS,
17 USAGE,
18 BTOP_LINE,
19 cpuPercent,
20 isBandOn,
21 memUsedBytes,
22 paneIdOf,
23 parseBattery,
24 parseLoad,
25 parsePositiveInt,
26 readingText,
27} from './kit'
28import { drawRow } from './row'
29
30const reading = atom({ plugin: 'sys', key: 'reading' } as const, null)
31const band = atom({ plugin: 'sys', key: 'band' } as const, '')
32
33// The reader's timer. A reload drops it with the environment; session.start
34// (raised again for a fresh load) starts it again.
35let ticker: Timer | null = null
36// One reading at a time: a slow command never stacks readings.
37let isReading = false
38// Bumped by every stop: a start that awaited across a later start or stop
39// sees a newer generation and installs no timer, so no timer is ever orphaned.
40let readerGen = 0
41// One btop open at a time: repeated presses or /sys btop never split extra panes.
42let isOpeningBtop = false
43
44const stopTick = () => {
45 readerGen += 1
46 ticker?.cancel()
47 ticker = null
48}
49
50/** A finished command's output, or null when it failed, timed out or could not start. */
51const outOf = (r: PromiseSettledResult<ProcessRunResult>): string | null =>
52 r.status === 'fulfilled' && r.value.exitCode === 0 ? r.value.stdout : null
53
54/** Runs the five commands side by side and reads what came back; a part that failed is null. */
55async function takeReading($: EngineInterface): Promise<SysReading> {
56 const [load, ncpu, memsize, vmStat, batt] = await Promise.allSettled([
57 $.process.run(['sysctl', '-n', 'vm.loadavg'], { timeoutMs: RUN_TIMEOUT_MS }),
58 $.process.run(['sysctl', '-n', 'hw.ncpu'], { timeoutMs: RUN_TIMEOUT_MS }),
59 $.process.run(['sysctl', '-n', 'hw.memsize'], { timeoutMs: RUN_TIMEOUT_MS }),
60 $.process.run(['vm_stat'], { timeoutMs: RUN_TIMEOUT_MS }),
61 $.process.run(['pmset', '-g', 'batt'], { timeoutMs: RUN_TIMEOUT_MS }),
62 ])
63 const loadAvg = parseLoad(outOf(load) ?? '')
64 const cores = parsePositiveInt(outOf(ncpu) ?? '')
65 const total = parsePositiveInt(outOf(memsize) ?? '')
66 const vm = outOf(vmStat)
67 const used = vm === null ? null : memUsedBytes(vm)
68 const pm = outOf(batt)
69 return {
70 cpu: loadAvg !== null && cores !== null ? cpuPercent(loadAvg, cores) : null,
71 ram: used !== null && total !== null ? { used, total } : null,
72 battery: pm === null ? null : parseBattery(pm),
73 readAt: await $.clock.now(),
74 }
75}
76
77/** One reading into $.state; skipped while another is under way. */
78async function refresh($: EngineInterface): Promise<SysReading | null> {
79 if (isReading) return null
80 isReading = true
81 try {
82 const r = await takeReading($)
83 await update($, reading, () => r)
84 return r
85 } finally {
86 isReading = false
87 }
88}
89
90/** The reader runs only while a terminal shows the session and the row is on. */
91async function isWanted($: EngineInterface, options: PluginOptions): Promise<boolean> {
92 if (!isBandOn(await read($, band), options.band)) return false
93 try {
94 return (await $.session.surfaces()).includes('terminal')
95 } catch {
96 return false
97 }
98}
99
100async function tick($: EngineInterface, options: PluginOptions): Promise<void> {
101 if (!(await isWanted($, options))) {
102 stopTick()
103 return
104 }
105 await refresh($)
106}
107
108/** Starts the reader when it is wanted: a first reading now (from a timer), then every 5 seconds. */
109async function startReader($: EngineInterface, options: PluginOptions): Promise<void> {
110 stopTick()
111 const gen = readerGen
112 if (!(await isWanted($, options))) return
113 // A later start or a stop ran while this one waited: it owns the reader now.
114 if (gen !== readerGen) return
115 $.clock.after(10, () => {
116 void refresh($).catch(() => {})
117 })
118 ticker = $.clock.every(TICK_MS, () => {
119 void tick($, options).catch(() => {})
120 })
121}
122
123/**
124 * Opens btop in a herdr pane below this one: split (no focus, the session's
125 * cwd), then run the btop line in the new pane. Answers what happened.
126 */
127async function openBtop($: EngineInterface): Promise<string> {
128 // Set before any await, so a second press in the same moment sees it.
129 if (isOpeningBtop) return `btop is already opening (/${COMMAND} btop)`
130 isOpeningBtop = true
131 try {
132 return await openBtopOnce($)
133 } finally {
134 isOpeningBtop = false
135 }
136}
137
138async function openBtopOnce($: EngineInterface): Promise<string> {
139 if ((await $.env.get('HERDR_ENV')) !== '1') return NEEDS_HERDR
140 try {
141 const cwd = await $.session.cwd()
142 const split = await $.process.run(['herdr', 'pane', 'split', '--current', '--direction', 'down', '--no-focus', '--cwd', cwd], {
143 timeoutMs: HERDR_TIMEOUT_MS,
144 })
145 const id = split.exitCode === 0 ? paneIdOf(split.stdout) : null
146 if (!id) return `btop: herdr pane split gave no pane (/${COMMAND} btop)`
147 const run = await $.process.run(['herdr', 'pane', 'run', id, BTOP_LINE], { timeoutMs: HERDR_TIMEOUT_MS })
148 if (run.exitCode !== 0) return `btop: herdr pane run failed in ${id} (/${COMMAND} btop)`
149 return `btop opened in pane ${id} below (/${COMMAND} btop)`
150 } catch (err) {
151 const why = err instanceof Error ? err.message : String(err)
152 return `btop did not open: ${why} (/${COMMAND} btop)`
153 }
154}
155
156/** The button's action: from a timer (never inside the press), the answer as a toast. */
157function btopFromButton($: EngineInterface): void {
158 $.clock.after(10, () => {
159 void openBtop($)
160 .then(text => $.ui.toast(text))
161 .catch(() => {})
162 })
163}
164
165export const register: Register = (on, options) => {
166 on('session.start', async ($, e, next) => {
167 const started = await next(e)
168 await $.command.register({
169 name: 'sys',
170 description: 'SYS row: CPU, RAM and battery now, open btop in a herdr pane, or turn the row on or off',
171 argumentHint: 'on | off | now | btop',
172 })
173 try {
174 const stored = await $.store.get(STORE_BAND)
175 await update($, band, () => (stored === 'on' || stored === 'off' ? stored : ''))
176 } catch {
177 // No store: the option applies.
178 }
179 await startReader($, options).catch(() => {})
180 return started
181 })
182
183 // The command and its matcher are spelled literally, so the host reads it as answering its own command.
184 on('command.run', { command: 'sys' }, async ($, e) => {
185 const [verb = 'now'] = e.args.trim().toLowerCase().split(/\s+/).filter(Boolean)
186 switch (verb) {
187 case 'on':
188 case 'off':
189 await $.store.set(STORE_BAND, verb)
190 await update($, band, () => verb)
191 if (verb === 'off') {
192 stopTick()
193 return { text: `SYS line off (no row, no reading; /${COMMAND} now and btop still work).` }
194 }
195 await startReader($, options)
196 return { text: 'SYS line on.' }
197 case 'now': {
198 const r = (await refresh($)) ?? (await read($, reading))
199 return { text: r ? readingText(r) : 'A reading is under way; try again in a moment.' }
200 }
201 case 'btop':
202 return { text: await openBtop($) }
203 default:
204 return { text: USAGE }
205 }
206 })
207
208 on('ui.render', { component: 'AbovePrompt' }, ($, e, next) => drawRow($, e, () => next(e), options, () => btopFromButton($)))
209}
210hooks/kit.ts 215 lines1// Shared by register.tsx and row.tsx: the look, the parsers of each command's
2// output (the arithmetic of herdr-setup/monitor/herdr_monitor.py,
3// sysinfo_parts and _mem_used_bytes) and the row's segments. The state atoms
4// are declared again in each file that reads or writes them: the engine reads
5// a state source only from a const of the calling file.
6import type { SysBattery, SysReading } from '../types'
7import type { RenderElement, RenderNode } from 'claude-code'
8
9export const CHIP = ' ▣ SYS ' // padded to the widest zehui-mods chip (HANDOFF) so the colour bars line up
10export const CHIP_FG = '#0b0b0f'
11export const CHIP_BG = '#2dd4bf'
12export const GREEN = '#22c55e'
13export const AMBER = '#f59e0b'
14export const RED = '#ef4444'
15export const BAR_CELLS = 6
16/** CPU and RAM turn amber at this percentage, red at the next. */
17export const AMBER_AT = 80
18export const RED_AT = 95
19/** The battery turns red under this percentage while it does not charge. */
20export const BATTERY_LOW = 15
21/** The reader's period. */
22export const TICK_MS = 5_000
23/** Each reader command's limit. */
24export const RUN_TIMEOUT_MS = 2_000
25/** Each herdr command's limit (the btop split). */
26export const HERDR_TIMEOUT_MS = 5_000
27export const COMMAND = 'sys'
28export const STORE_BAND = 'sys.band'
29/** Run by the new pane's shell, so $HOME expands there. */
30export const BTOP_LINE = 'XDG_CONFIG_HOME=$HOME/.config/btop-mini btop'
31export const NEEDS_HERDR = `btop needs a herdr pane (/${COMMAND} btop)`
32export const USAGE = `Usage: /${COMMAND} on | off | now | btop`
33
34const GIB = 1024 ** 3
35
36/** The first figure of `sysctl -n vm.loadavg` (`{ 2.12 1.59 1.46 }`): the 1-minute load. */
37export function parseLoad(stdout: string): number | null {
38 const m = /(\d+(?:\.\d+)?)/.exec(stdout)
39 return m ? Number(m[1]) : null
40}
41
42/** A positive whole number (`sysctl -n hw.ncpu`, `hw.memsize`), else null. */
43export function parsePositiveInt(stdout: string): number | null {
44 const n = Number.parseInt(stdout.trim(), 10)
45 return Number.isFinite(n) && n > 0 ? n : null
46}
47
48/** Load over cores as a whole percentage, truncated as the monitor's int() does. */
49export const cpuPercent = (load: number, cores: number): number => Math.floor((load / cores) * 100 + 1e-9)
50
51/**
52 * Bytes in use from vm_stat: active + wired down + occupied by compressor
53 * pages times the page size in its header (4096 when the header lacks one);
54 * null when any of the three is missing.
55 */
56export function memUsedBytes(vmStat: string): number | null {
57 const header = /page size of (\d+) bytes/.exec(vmStat)
58 const page = header ? Number(header[1]) : 4096
59 let pages = 0
60 for (const key of ['Pages active', 'Pages wired down', 'Pages occupied by compressor']) {
61 const m = new RegExp(`${key}:\\s+(\\d+)`).exec(vmStat)
62 if (!m) return null
63 pages += Number(m[1])
64 }
65 return pages * page
66}
67
68/**
69 * The battery from `pmset -g batt`: the first `n%`, and whether it charges
70 * (`; charging`, `; charged`, `; finishing charge`). Null with no percent: a
71 * machine with no battery.
72 */
73export function parseBattery(stdout: string): SysBattery | null {
74 const m = /(\d+)%/.exec(stdout)
75 if (!m) return null
76 return { percent: Number(m[1]), isCharging: /;\s*(charging|charged|finishing charge)/i.test(stdout) }
77}
78
79/** Green under 80%, amber from 80%, red from 95%. */
80export const levelColor = (pct: number): string => (pct >= RED_AT ? RED : pct >= AMBER_AT ? AMBER : GREEN)
81
82/** Red under 15% while not charging; otherwise the default colour. */
83export const batteryColor = (b: SysBattery): string | undefined => (b.percent < BATTERY_LOW && !b.isCharging ? RED : undefined)
84
85/** Filled cells of the 6-cell bar: at least one above 0%, all six from 100%. */
86export const barCells = (pct: number, cells = BAR_CELLS): number =>
87 pct <= 0 ? 0 : Math.min(cells, Math.max(1, Math.round((pct / 100) * cells)))
88
89export const ramPercent = (ram: { used: number; total: number }): number => (ram.used / ram.total) * 100
90
91/** `28.9/64G`: used to one decimal, the total whole, in GiB. */
92export const ramText = (ram: { used: number; total: number }): string => `${(ram.used / GIB).toFixed(1)}/${Math.round(ram.total / GIB)}G`
93
94/** The row's switch: /sys on|off when set, else the `band` option (anything but `off` is on). */
95export const isBandOn = (stored: '' | 'on' | 'off', option: unknown): boolean => (stored ? stored === 'on' : option !== 'off')
96
97/** One run of text in the row: its words and how they are drawn. */
98export type Seg = { text: string; color?: string; bg?: string; isBold?: boolean; isDim?: boolean }
99
100export const widthOf = (segs: readonly Seg[]): number => segs.reduce((w, s) => w + [...s.text].length, 0)
101
102const bar = (pct: number, color: string): Seg[] => {
103 const n = barCells(pct)
104 return [
105 ...(n > 0 ? [{ text: '▰'.repeat(n), color }] : []),
106 ...(n < BAR_CELLS ? [{ text: '▱'.repeat(BAR_CELLS - n), isDim: true }] : []),
107 ]
108}
109
110/**
111 * The row's segments after the chip, fitted to `room` columns: with both bars
112 * and the battery first, then without the bars, then without the battery.
113 * Empty when the reading has nothing to show.
114 */
115export function readingSegs(r: SysReading | null, room: number): Seg[] {
116 if (!r) return []
117 const build = (isBars: boolean, isBattery: boolean): Seg[] => {
118 const parts: Seg[][] = []
119 if (r.cpu !== null) {
120 const c = levelColor(r.cpu)
121 parts.push([{ text: 'CPU ' }, { text: `${r.cpu}%`, color: c }, ...(isBars ? [{ text: ' ' }, ...bar(r.cpu, c)] : [])])
122 }
123 if (r.ram) {
124 const pct = ramPercent(r.ram)
125 const c = levelColor(pct)
126 parts.push([{ text: 'RAM ' }, { text: ramText(r.ram), color: c }, ...(isBars ? [{ text: ' ' }, ...bar(pct, c)] : [])])
127 }
128 if (r.battery && isBattery) {
129 const c = batteryColor(r.battery)
130 parts.push([{ text: `⚡ ${r.battery.percent}%`, ...(c ? { color: c, isBold: true } : {}) }])
131 }
132 return parts.flatMap((p, i) => [{ text: i === 0 ? ' ' : ' · ', isDim: i > 0 }, ...p])
133 }
134 const tries = [build(true, true), build(false, true), build(false, false)]
135 const fits = tries.find(t => widthOf(t) <= room)
136 const chosen = fits ?? tries[tries.length - 1]!
137 return chosen.length > 0 ? chosen : []
138}
139
140/** `CPU 6% · RAM 28.9/64G · ⚡ 65% (discharging)`: the /sys now answer. */
141export function readingText(r: SysReading): string {
142 const parts: string[] = []
143 if (r.cpu !== null) parts.push(`CPU ${r.cpu}%`)
144 if (r.ram) parts.push(`RAM ${ramText(r.ram)} (${Math.round(ramPercent(r.ram))}%)`)
145 if (r.battery) parts.push(`⚡ ${r.battery.percent}% (${r.battery.isCharging ? 'charging' : 'not charging'})`)
146 return parts.length > 0 ? parts.join(' · ') : 'No reading: sysctl, vm_stat and pmset gave nothing.'
147}
148
149/** The new pane's id from `herdr pane split`'s JSON (`.result.pane.pane_id`), else null. */
150export function paneIdOf(stdout: string): string | null {
151 try {
152 const j = JSON.parse(stdout) as { result?: { pane?: { pane_id?: unknown } } }
153 const id = j.result?.pane?.pane_id
154 return typeof id === 'string' && id.length > 0 ? id : typeof id === 'number' ? String(id) : null
155 } catch {
156 return null
157 }
158}
159
160// Stack order of the zehui-mods band rows (plan 2026-10-20-boards-codemap-recap,
161// section 0): SYS 10, PLAN 20, HANDOFF 30, CACHE 40, AGENTS 50, GOAL 60, HERE 70,
162// NEXT 80. Copied unchanged from the canonical
163// ~/.claude/skills/building-claude-code-mods/references/stack-row.tsx; change it
164// there first, then in every band mod.
165
166/** The rows each zehui-mods band draws, by stack order. */
167export const STACK = { sys: 10, plan: 20, handoff: 30, cache: 40, agents: 50, goal: 60, here: 70, next: 80 } as const
168
169/** A row's key: `zm-<order>-<name>`. */
170export const stackKey = (name: keyof typeof STACK): string => `zm-${STACK[name]}-${name}`
171
172type Loose = { type?: unknown; props?: Record<string, unknown> | null; part?: unknown; children?: unknown }
173
174/** The order of a zehui-mods row (`zm-<order>-<name>`), or null for any other node. */
175export const stackOrderOf = (node: unknown): number | null => {
176 const key = node && typeof node === 'object' ? (node as Loose).props?.key : undefined
177 const m = typeof key === 'string' ? /^zm-(\d+)-/.exec(key) : null
178 return m ? Number(m[1]) : null
179}
180
181/**
182 * The column a band hook beneath built: a Box whose only prop is
183 * `flexDirection: 'column'` and whose direct children hold a zm row. A Box with
184 * other props (a border, a padding) is another mod's own drawing, kept whole.
185 */
186const isStack = (node: Loose): node is Loose & { children: unknown[] } =>
187 node.type === 'Box' &&
188 node.props?.flexDirection === 'column' &&
189 Object.keys(node.props).every(k => k === 'flexDirection') &&
190 Array.isArray(node.children) &&
191 node.children.some(c => stackOrderOf(c) !== null)
192
193/**
194 * `row` put in its slot among the zm rows of `below` (what `next(e)` drew):
195 * zm rows sorted by their number, then every other child in its original order.
196 * Nothing beneath (null, a failed next, the engine placeholder): the row alone.
197 */
198export function stackRow(row: RenderElement, below: unknown): RenderElement {
199 const tree = below && typeof below === 'object' && (below as Loose).type !== 'engine' ? (below as Loose) : null
200 if (!tree) return row
201 let items: unknown[] = [tree]
202 if (isStack(tree)) {
203 items = [...tree.children]
204 // The column's mark (whose drawing started there) moves to the one row
205 // that carries none, its own; the engine lets a mark move, never multiply.
206 const bare = items.filter(c => c && typeof c === 'object' && !(c as Loose).part)
207 if (tree.part && bare.length === 1) items = items.map(c => (c === bare[0] ? { ...(c as object), part: tree.part } : c))
208 }
209 const rows = items.filter(c => stackOrderOf(c) !== null)
210 const others = items.filter(c => stackOrderOf(c) === null)
211 // A stable sort: two rows of one number keep their order, this row after.
212 const sorted = [...rows, row].sort((a, b) => (stackOrderOf(a) ?? 0) - (stackOrderOf(b) ?? 0))
213 return { type: 'Box', props: { flexDirection: 'column' }, children: [...sorted, ...others] as RenderNode[] }
214}
215hooks/row.tsx 60 lines1// The ▣ SYS row above the prompt: CPU load and RAM in use, each with a 6-cell
2// bar in traffic-light colours, and the battery, then a framed btop button.
3// Its row sits in the zehui-mods stack
4// by its order (stackRow); other mods' bands follow.
5import { atom, read } from 'claude-code'
6import type { EngineInterface, PluginOptions, RenderElement, RenderInput } from 'claude-code'
7
8import { CHIP, CHIP_BG, CHIP_FG, isBandOn, readingSegs, stackKey, stackRow } from './kit'
9import type { Seg } from './kit'
10
11const reading = atom({ plugin: 'sys', key: 'reading' } as const, null)
12const band = atom({ plugin: 'sys', key: 'band' } as const, '')
13
14/** The framed button's width beside the line: `[ btop ]` and a one-column gap. */
15const BUTTON_W = 'btop'.length + 4 + 1
16
17export async function drawRow(
18 $: EngineInterface,
19 e: RenderInput<'AbovePrompt'>,
20 pass: () => Promise<RenderElement>,
21 options: PluginOptions,
22 onBtop: () => void,
23) {
24 if (e.surface !== 'terminal' || e.props.hasSurvey) return pass()
25 if (!isBandOn(await read($, band), options.band)) return pass()
26 const room = (e.props.bodyColumns > 0 ? e.props.bodyColumns : 80) - BUTTON_W - [...CHIP].length
27 const body = readingSegs(await read($, reading), room)
28 if (body.length === 0) return pass()
29
30 // Other mods' bands beneath this row; stackRow puts this row in its slot
31 // among the zehui-mods rows and never nests the engine's own placeholder.
32 const below = await pass().catch(() => null)
33
34 const segs: Seg[] = [{ text: CHIP, color: CHIP_FG, bg: CHIP_BG, isBold: true }, ...body]
35 const { Box, Text, Button } = $.ui.resolve(e)
36 const row = (
37 <Box key={stackKey('sys')} flexDirection="row" gap={1}>
38 <Box key="sys-line" flexGrow={1} flexShrink={1}>
39 <Text wrap="truncate-end">
40 {segs.map((g, i) => (
41 <Text
42 key={`seg-${i}`}
43 {...(g.color ? { color: g.color } : {})}
44 {...(g.bg ? { backgroundColor: g.bg } : {})}
45 {...(g.isBold ? { bold: true } : {})}
46 {...(g.isDim ? { dimColor: true } : {})}
47 >
48 {g.text}
49 </Text>
50 ))}
51 </Text>
52 </Box>
53 <Box flexShrink={0}>
54 <Button key="sys-btop" label="btop" onPress={() => onBtop()} />
55 </Box>
56 </Box>
57 )
58 return stackRow(row, below)
59}
60types/index.d.ts 33 lines1// The sys mod's state contract.
2
3/** The battery as `pmset -g batt` reports it. */
4export type SysBattery = {
5 /** Charge, a whole percentage. */
6 percent: number
7 /** True while it charges or is full (`charging`, `charged`, `finishing charge`); false when discharging or on AC without charging. */
8 isCharging: boolean
9}
10
11/** One reading of the machine; a part that could not be read is null and left out of the row. */
12export type SysReading = {
13 /** The 1-minute load average over the cores, a whole percentage (may pass 100). */
14 cpu: number | null
15 /** Active + wired + compressor pages from vm_stat, and hw.memsize, in bytes. */
16 ram: { used: number; total: number } | null
17 /** Null on a machine with no battery. */
18 battery: SysBattery | null
19 /** When the reading was taken, `$.clock.now()` milliseconds. */
20 readAt: number
21}
22
23declare module 'claude-code' {
24 interface PluginState {
25 sys: {
26 /** The latest reading; null until the first one. */
27 reading: SysReading | null
28 /** /sys on|off as kept in the store; '' while the plugin option applies. */
29 band: '' | 'on' | 'off'
30 }
31 }
32}
33