SLOPSHOPPER

sys

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…

newbandcommandtoastprocesstimer
v0.1.1MITupdated 2026-10-20ZH-L1N/agent-window-mods/sys
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · sys
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /sys ⎿ sys: No reading: sysctl, vm_stat and pmset gave nothing. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

sys

A ▣ SYS row above the prompt: how busy the machine is, read every 5 seconds.

 ▣ SYS     CPU 6% ▰▱▱▱▱▱ · RAM 28.9/64G ▰▰▰▱▱▱ · ⚡ 65%   [ btop ]
  • CPU: the 1-minute load average over the cores, as a percentage (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.
  • RAM: active + wired + compressor pages from vm_stat, times the page size in its header, over sysctl -n hw.memsize, in GiB.
  • ⚡: the battery's percentage from pmset -g batt. A machine with no battery has no ⚡ segment.
  • Colours: CPU and RAM green, amber from 80%, red from 95% (the figure and the bar's filled cells; empty cells dim). The battery is red under 15% while it is not charging (discharging, or on AC without charging).
  • When the band is narrow the bars go first, then the battery.

The figures are the same arithmetic as herdr-setup/monitor/herdr_monitor.py (sysinfo_parts, _mem_used_bytes).

Reader

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.

btop

The btop button and /sys btop open btop in a herdr pane below this one:

  1. herdr pane split --current --direction down --no-focus --cwd <session cwd>
  2. 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:

ButtonCommand
btop/sys btop

Command

/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.

Options

OptionValuesDefault
bandon, offon

Install

From the local marketplace folder (~/.claude/mods):

claude plugin install sys@zehui-mods --scope user

Notes

  • No network: the mod runs sysctl, vm_stat, pmset and, for btop, herdr, and nothing else. No model is called.
  • Not in the row: the clock (the status line has it) and today's tokens (the PLAN row has them, token-weather local change 10).
  • The row composes with the other bands: keyed 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.
Source 4 files
hooks/register.tsx 210 lines
1// 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}
210
hooks/kit.ts 215 lines
1// 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}
215
hooks/row.tsx 60 lines
1// 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}
60
types/index.d.ts 33 lines
1// 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