SLOPSHOPPER

session-stats

/session-stats opens a live pane with CPU, RAM and the process tree of this Claude session

newpanebandcommandprocesstimer
v0.2.0MITupdated 2026-10-15toml0006/claude-session-stats
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · session-stats
│ ┃ Session stats ✕ › fix the failing auth test and add an audit log call │ ┃ Error: no claude process above pid │ ⏺ 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 │ │ › /session-stats │ ⎿ session-stats: Session stats pane opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Session stats
Error: no claude process above pid
README

session-stats

A Claude Code mod that shows what your current session is costing your machine. Type /session-stats and a pane opens with:

The session-stats pane: CPU, RAM, network, disk and energy with sparklines, then the process tree

  • CPU and RAM for the session's whole process tree (the claude process plus every shell, build, test runner and stdio MCP server it started), with sparklines of up to the last 4 minutes
  • network (down and up), disk (read and write) and energy (watts, and joules since sampling began) for the same tree, each with its own sparkline
  • the session's share of your cores and memory, process count, uptime and load average
  • a process table, indented as a tree, busiest first, with each process's power draw

It samples every 2 seconds while the pane or the bar is showing, and stops when neither is.

Bar mode

/session-stats bar toggles a one-line strip above the prompt:

The session-stats bar above the prompt: CPU and RAM numbers, each followed by a sparkline

Each history is up to 30 cells wide (one minute) from the start and fills left to right, then scrolls. Click the strip to open the full pane. The setting is remembered across sessions; /session-stats bar on and /session-stats bar off set it directly.

Stopping it

/session-stats stop (or off, or kill) closes the pane, turns the bar off and stops all sampling, so the mod runs nothing until you call it again.

Install

At the prompt of a Claude Code terminal session:

/plugin install session-stats --marketplace toml0006/claude-session-stats

Answer y to add the marketplace, then pick a scope. From a shell:

claude plugin install session-stats --marketplace toml0006/claude-session-stats

Limits

  • macOS only. It reads sysctl hw.memsize and vm.loadavg and macOS ps output.
  • Subagents run inside the main claude process, so they're part of its numbers, not separate rows.
  • MCP servers reached over HTTP run outside the session and aren't counted.
  • CPU % is ps's smoothed recent average and can pass 100% on multiple cores.
  • RAM sums each process's resident memory, so shared pages count more than once.
  • Network comes from nettop, which takes about 5 seconds per sample, so it updates about every 5 seconds. A background session (claude --bg) sends its API traffic through the shared daemon, so that traffic isn't counted under the session.
  • Disk and energy come from proc_pid_rusage (what Activity Monitor reads), called through the system python3. They need the Xcode Command Line Tools; without them the pane says so rather than triggering an install prompt. Energy needs macOS 13 or later.

Develop

claude plugin validate .
claude plugin test .
claude --plugin-dir .

License

MIT

Source 3 files
hooks/register.tsx 270 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { Io, Stats } from '../types'
5import type { Counters } from './stats'
6import { NET_PROBE, PROBE, bytes, gb, ioProbe, parseNettop, parseProbe, parseRusage, rates, sparkline } from './stats'
7
8const PANE = 'session-stats'
9const PERIOD_MS = 2000
10const HISTORY = 120
11// Each history in the bar is at most this many cells (one minute of samples)
12const BAR_SPAN = 30
13const BAR_KEY = 'bar'
14const EMPTY: Stats = {
15  snapshot: null,
16  error: null,
17  cpuHistory: [],
18  ramHistory: [],
19  io: null,
20  isRusageAvailable: true,
21  net: null,
22  netHistory: [],
23  diskHistory: [],
24  energyHistory: [],
25}
26const stats = atom({ plugin: 'session-stats', key: 'stats' } as const, EMPTY)
27const isBarOn = atom({ plugin: 'session-stats', key: 'isBarOn' } as const, false)
28
29const cpuColor = (cpu: number) => (cpu >= 100 ? 'red' : cpu >= 50 ? 'yellow' : 'green')
30const ramColor = (kb: number) => (kb >= 4194304 ? 'red' : kb >= 2097152 ? 'yellow' : 'green')
31
32// One sampler serves both the bar and the pane, and runs only while either
33// is showing. A reload resets these; session.start restarts it for the bar.
34let timer: Timer | undefined
35let isPaneOpen = false
36
37// The previous cumulative counters, to turn the next ones into rates
38let prevRusage: { at: number; counters: Counters } | undefined
39let prevNet: { at: number; counters: Counters } | undefined
40let isNetBusy = false
41
42const push = (list: readonly number[], v: number) => [...list, v].slice(-HISTORY)
43
44// The host keeps $.state across reloads and updates, so a value written by an
45// older version can lack fields this one reads: fill them in on every read.
46async function readStats($: EngineInterface): Promise<Stats> {
47  return { ...EMPTY, ...(await read($, stats)) }
48}
49
50async function updateStats($: EngineInterface, fn: (s: Stats) => Stats) {
51  await update($, stats, s => fn({ ...EMPTY, ...s }))
52}
53
54async function sample($: EngineInterface) {
55  try {
56    const { stdout } = await $.process.run(PROBE, { env: { LC_ALL: 'C' }, timeoutMs: 5000 })
57    const snap = parseProbe(stdout)
58    const pids = snap.procs.map(p => p.pid)
59
60    const probe = ioProbe(pids)
61    const usage = parseRusage((await $.process.run(probe.argv, { env: probe.env, timeoutMs: 5000 })).stdout)
62    const at = await $.clock.now()
63    let io: Io | null = null
64    let seconds = 0
65    if (prevRusage && usage.size > 0) {
66      seconds = (at - prevRusage.at) / 1000
67      const { total, perPid } = rates(prevRusage.counters, usage, seconds, pids)
68      const hasEnergy = [...usage.values()].every(c => (c[2] ?? -1) >= 0)
69      for (const p of snap.procs) {
70        const r = perPid.get(p.pid)
71        p.watts = r && hasEnergy ? (r[2] ?? 0) / 1e9 : null
72      }
73      const watts = hasEnergy ? (total[2] ?? 0) / 1e9 : 0
74      io = { diskRead: total[0] ?? 0, diskWrite: total[1] ?? 0, watts, hasEnergy, joules: 0 }
75    }
76    prevRusage = usage.size > 0 ? { at, counters: usage } : undefined
77
78    await updateStats($, s => ({
79      ...s,
80      snapshot: snap,
81      error: null,
82      cpuHistory: push(s.cpuHistory, snap.cpu),
83      ramHistory: push(s.ramHistory, snap.rssKb),
84      isRusageAvailable: usage.size > 0,
85      io: io ? { ...io, joules: (s.io?.joules ?? 0) + io.watts * seconds } : s.io,
86      diskHistory: io ? push(s.diskHistory, io.diskRead + io.diskWrite) : s.diskHistory,
87      energyHistory: io?.hasEnergy ? push(s.energyHistory, io.watts) : s.energyHistory,
88    }))
89    if (!isNetBusy) void sampleNet($, pids)
90  } catch (err) {
91    await updateStats($, s => ({ ...s, error: String(err) }))
92  }
93}
94
95async function sampleNet($: EngineInterface, pids: readonly number[]) {
96  isNetBusy = true
97  try {
98    const counters = parseNettop((await $.process.run(NET_PROBE, { timeoutMs: 15000 })).stdout)
99    const at = await $.clock.now()
100    if (prevNet) {
101      const { total } = rates(prevNet.counters, counters, (at - prevNet.at) / 1000, pids)
102      const net = { bytesIn: total[0] ?? 0, bytesOut: total[1] ?? 0 }
103      await updateStats($, s => ({ ...s, net, netHistory: push(s.netHistory, net.bytesIn + net.bytesOut) }))
104    }
105    prevNet = { at, counters }
106  } catch {
107    // Network is a nice-to-have: a failed nettop leaves the last reading
108  } finally {
109    isNetBusy = false
110  }
111}
112
113async function syncSampler($: EngineInterface) {
114  const isNeeded = isPaneOpen || (await read($, isBarOn))
115  if (isNeeded && !timer) {
116    await sample($)
117    timer = $.clock.every(PERIOD_MS, () => void sample($))
118  } else if (!isNeeded && timer) {
119    timer.cancel()
120    timer = undefined
121  }
122}
123
124// Closes the pane, turns the bar off (remembered), and stops every sampler
125async function stopAll($: EngineInterface) {
126  isPaneOpen = false
127  await $.store.set(BAR_KEY, false)
128  await update($, isBarOn, () => false)
129  await syncSampler($)
130  prevRusage = undefined
131  prevNet = undefined
132  await $.ui.close({ id: PANE }).catch(() => undefined)
133  return 'Session stats stopped: pane closed, bar off, nothing sampling. /session-stats starts it again.'
134}
135
136async function openPane($: EngineInterface) {
137  const opened = await $.ui.open({ id: PANE, title: 'Session stats' })
138  if (!opened.isPlaced) return `Session stats pane not shown: ${opened.reason}`
139  isPaneOpen = true
140  await syncSampler($)
141  return 'Session stats pane opened.'
142}
143
144export const register: Register = on => {
145  on('session.start', async ($, e, next) => {
146    await $.command.register({
147      name: 'session-stats',
148      description: 'Show live CPU, RAM and processes of this session; "bar" toggles the strip above the prompt, "stop" shuts everything off',
149      argumentHint: '[bar [on|off] | stop]',
150    })
151    const isOn = (await $.store.get(BAR_KEY)) === true
152    await update($, isBarOn, () => isOn)
153    // A reload keeps an open pane open but forgets it here; ask, so it keeps updating
154    isPaneOpen = await $.ui
155      .panes()
156      .then(panes => panes.some(pane => pane.id === PANE))
157      .catch(() => false)
158    await syncSampler($)
159    return next(e)
160  })
161
162  on('command.run', { command: 'session-stats' }, async ($, e) => {
163    const [sub, arg] = e.args.trim().split(/\s+/)
164    if (sub === 'stop' || sub === 'off' || sub === 'kill') return { text: await stopAll($) }
165    if (sub !== 'bar') return { text: await openPane($) }
166
167    const isOn = arg === 'on' ? true : arg === 'off' ? false : !(await read($, isBarOn))
168    await $.store.set(BAR_KEY, isOn)
169    await update($, isBarOn, () => isOn)
170    await syncSampler($)
171    return { text: `Session stats bar ${isOn ? 'on' : 'off'}.` }
172  })
173
174  on('ui.close', { id: PANE }, async ($, e, next) => {
175    isPaneOpen = false
176    await syncSampler($)
177    return next(e)
178  })
179
180  // The strip above the prompt: numbers first, then each history at its full
181  // width from the first sample, filling left to right and then scrolling. One
182  // Button, so a click anywhere on it opens the pane.
183  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
184    if (e.props.hasSurvey || !(await read($, isBarOn))) return next(e)
185
186    const { Button, Text } = $.ui.resolve(e)
187    const { snapshot: s, cpuHistory, ramHistory } = await readStats($)
188    const width = e.props.bodyColumns ?? e.viewport?.columns ?? 80
189    // "⚙ CPU 100% " + " " + " RAM 10.0G " + " " is 26 cells around the two histories
190    const span = Math.max(6, Math.min(BAR_SPAN, Math.floor((width - 26) / 2)))
191    const cpu = sparkline(cpuHistory.slice(-span))
192    const ram = sparkline(ramHistory.slice(-span))
193
194    return (
195      <Button key="open-pane" plain onPress={() => void openPane($)}>
196        <Text dimColor>⚙ CPU </Text>
197        <Text color={cpuColor(s?.cpu ?? 0)}>{(s ? `${s.cpu.toFixed(0)}%` : '…').padStart(4)} </Text>
198        <Text color="cyan">{cpu}</Text>
199        <Text dimColor>{'·'.repeat(span - cpu.length)}  RAM </Text>
200        <Text color={ramColor(s?.rssKb ?? 0)}>{(s ? `${gb(s.rssKb)}G` : '…').padStart(5)} </Text>
201        <Text color="magenta">{ram}</Text>
202        <Text dimColor>{'·'.repeat(span - ram.length)}</Text>
203      </Button>
204    )
205  })
206
207  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
208    const { Box, Text } = $.ui.resolve(e)
209    const { snapshot: s, error, cpuHistory, ramHistory, io, net, netHistory, diskHistory, energyHistory, isRusageAvailable } =
210      await readStats($)
211    const width = e.props.bodyColumns ?? e.viewport?.columns ?? 80
212    const noRusage = isRusageAvailable ? null : 'needs the Xcode Command Line Tools'
213
214    if (!s) return <Text dimColor>{error ?? 'Sampling…'}</Text>
215
216    const machinePct = (s.cpu / s.cores).toFixed(0)
217    const ramPct = ((s.rssKb * 1024 * 100) / s.memBytes).toFixed(1)
218    const spark = Math.max(10, Math.min(HISTORY, width - 14))
219    const room = Math.max(3, (e.viewport?.rows ?? 30) - 18)
220    const nameCols = Math.max(10, width - 31)
221    const pad = (v: string, n: number) => v.padStart(n)
222
223    return (
224      <Box flexDirection="column">
225        <Text>
226          <Text bold>CPU </Text>
227          <Text color={cpuColor(s.cpu)}>{s.cpu.toFixed(0)}%</Text>
228          <Text dimColor> ({machinePct}% of {s.cores} cores)</Text>
229        </Text>
230        <Text color="cyan">{sparkline(cpuHistory.slice(-spark))}</Text>
231        <Text>
232          <Text bold>RAM </Text>
233          <Text color={ramColor(s.rssKb)}>{gb(s.rssKb)} GB</Text>
234          <Text dimColor> ({ramPct}% of {gb(s.memBytes / 1024)} GB)</Text>
235        </Text>
236        <Text color="magenta">{sparkline(ramHistory.slice(-spark))}</Text>
237        <Text>
238          <Text bold>NET </Text>
239          {net ? `↓ ${bytes(net.bytesIn)}/s  ↑ ${bytes(net.bytesOut)}/s` : <Text dimColor>measuring…</Text>}
240        </Text>
241        <Text color="blue">{sparkline(netHistory.slice(-spark))}</Text>
242        <Text>
243          <Text bold>DISK </Text>
244          {io ? `read ${bytes(io.diskRead)}/s  write ${bytes(io.diskWrite)}/s` : <Text dimColor>{noRusage ?? 'measuring…'}</Text>}
245        </Text>
246        <Text color="yellow">{sparkline(diskHistory.slice(-spark))}</Text>
247        <Text>
248          <Text bold>ENERGY </Text>
249          {io?.hasEnergy ? `${io.watts.toFixed(2)} W` : <Text dimColor>{io ? 'needs macOS 13 or later' : noRusage ?? 'measuring…'}</Text>}
250          {io?.hasEnergy && <Text dimColor> ({io.joules.toFixed(0)} J since sampling began)</Text>}
251        </Text>
252        <Text color="red">{sparkline(energyHistory.slice(-spark))}</Text>
253        <Text dimColor>
254          {s.procs.length} processes · session pid {s.rootPid} up {s.procs[0]?.elapsed} · load {s.load.map(l => l.toFixed(2)).join(' ')}
255        </Text>
256        <Text> </Text>
257        <Text bold>{'  PID  CPU%   MEM     W NAME'}</Text>
258        {s.procs.slice(0, room).map(p => (
259          <Text>
260            {pad(String(p.pid), 5)} {pad(p.cpu.toFixed(1), 5)} {pad(gb(p.rssKb) + 'G', 5)} {pad(p.watts == null ? '–' : p.watts.toFixed(2), 5)}{' '}
261            <Text dimColor={p.cpu < 1}>{('  '.repeat(p.depth) + p.name).slice(0, nameCols)}</Text>
262          </Text>
263        ))}
264        {s.procs.length > room && <Text dimColor>…{s.procs.length - room} more</Text>}
265        {error && <Text color="red">last sample failed: {error}</Text>}
266      </Box>
267    )
268  })
269}
270
hooks/stats.ts 172 lines
1import type { Proc, Snapshot } from '../types'
2
3// One shell call gathers everything: the spawner's pid (the engine, or a child
4// of it), machine facts, then every process. LC_ALL=C keeps ps's decimals dots.
5export const PROBE = [
6  '/bin/sh',
7  '-c',
8  'echo $PPID; sysctl -n hw.ncpu hw.memsize vm.loadavg; ps -axo pid=,ppid=,pcpu=,rss=,etime=,comm=',
9] as const
10
11const ROW = /^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+(\d+)\s+(\S+)\s+(.*)$/
12
13// "claude bg-spare" and "/Users/x/.local/bin/claude" both name a claude process
14const isClaude = (name: string) => name.split(' ')[0]?.split('/').pop() === 'claude'
15
16// A full path may hold spaces ("Application Support"); a renamed title holds words
17const shortName = (name: string) => {
18  if (name.startsWith('/')) return name.split('/').pop() ?? name
19  const [head = '', ...rest] = name.split(' ')
20  return [head.split('/').pop(), ...rest].join(' ')
21}
22
23export function parseProbe(stdout: string): Snapshot {
24  const [spawner = '', ncpu, memsize, loadavg = '', ...rows] = stdout.trim().split('\n')
25
26  const all = new Map<number, Omit<Proc, 'depth' | 'watts'>>()
27  for (const row of rows) {
28    const m = ROW.exec(row)
29    if (!m) continue
30    const pid = Number(m[1])
31    all.set(pid, {
32      pid,
33      ppid: Number(m[2]),
34      cpu: Number(m[3]),
35      rssKb: Number(m[4]),
36      elapsed: m[5] ?? '',
37      name: (m[6] ?? '').trim(),
38    })
39  }
40
41  // The session is the nearest claude ancestor of whatever ran the probe
42  let rootPid = Number(spawner)
43  while (rootPid > 1 && !isClaude(all.get(rootPid)?.name ?? '')) {
44    rootPid = all.get(rootPid)?.ppid ?? 1
45  }
46  if (rootPid <= 1) throw new Error(`no claude process above pid ${spawner}`)
47
48  const children = new Map<number, Omit<Proc, 'depth' | 'watts'>[]>()
49  for (const p of all.values()) {
50    if (!children.has(p.ppid)) children.set(p.ppid, [])
51    children.get(p.ppid)!.push(p)
52  }
53
54  // Depth-first so the table reads as a tree; busiest siblings first. The probe's
55  // own sh and ps are left out, since they exist only because we asked.
56  const probePids = new Set<number>()
57  for (const sh of children.get(Number(spawner)) ?? []) {
58    const ps = (children.get(sh.pid) ?? []).find(c => shortName(c.name) === 'ps')
59    if (shortName(sh.name) === 'sh' && ps) probePids.add(sh.pid).add(ps.pid)
60  }
61  const procs: Proc[] = []
62  // The io probes (nettop, and python under sh) are the session's direct children
63  const isIoProbe = (p: Omit<Proc, 'depth' | 'watts'>, depth: number) =>
64    depth === 1 && ['nettop', 'Python', 'python3'].includes(shortName(p.name))
65  const walk = (p: Omit<Proc, 'depth' | 'watts'>, depth: number) => {
66    if (probePids.has(p.pid) || isIoProbe(p, depth)) return
67    procs.push({ ...p, depth, name: shortName(p.name), watts: null })
68    const kids = [...(children.get(p.pid) ?? [])].sort((a, b) => b.cpu - a.cpu || b.rssKb - a.rssKb)
69    for (const k of kids) walk(k, depth + 1)
70  }
71  walk(all.get(rootPid)!, 0)
72
73  return {
74    rootPid,
75    rootName: shortName(all.get(rootPid)!.name),
76    cores: Number(ncpu),
77    memBytes: Number(memsize),
78    load: loadavg.replace(/[{}]/g, '').trim().split(/\s+/).map(Number),
79    cpu: procs.reduce((s, p) => s + p.cpu, 0),
80    rssKb: procs.reduce((s, p) => s + p.rssKb, 0),
81    procs,
82  }
83}
84
85const BARS = '▁▂▃▄▅▆▇█'
86
87export function sparkline(values: readonly number[], max?: number): string {
88  const top = max ?? Math.max(1, ...values)
89  return values.map(v => BARS[Math.min(7, Math.max(0, Math.round((v / top) * 7)))]).join('')
90}
91
92export const gb = (kb: number) => (kb / 1048576).toFixed(kb >= 10485760 ? 0 : 1)
93
94// Disk and energy per process come from proc_pid_rusage (what Activity Monitor
95// reads), called through the system python. It runs only where the Command
96// Line Tools are installed, so a bare Mac never sees an install prompt; there
97// the probe prints nothing and the pane says why.
98const RUSAGE_PY = `
99import ctypes, sys
100lib = ctypes.CDLL('/usr/lib/libproc.dylib')
101class RU(ctypes.Structure):
102    _fields_ = [('uuid', ctypes.c_uint8 * 16)] + [('f%d' % i, ctypes.c_uint64) for i in range(56)]
103for pid in map(int, sys.argv[1:]):
104    r = RU()
105    if lib.proc_pid_rusage(pid, 6, ctypes.byref(r)) == 0:
106        print(pid, r.f16, r.f17, r.f40)
107    elif lib.proc_pid_rusage(pid, 4, ctypes.byref(r)) == 0:
108        print(pid, r.f16, r.f17, -1)
109`
110// Fields after the uuid: 16 diskio_bytesread, 17 diskio_byteswritten, and in
111// rusage_info_v6 (macOS 13+) 40 energy_nj. v4's billed_energy reads 0 on Apple
112// silicon, so before v6 the energy column is -1, unknown.
113
114export const ioProbe = (pids: readonly number[]) => ({
115  argv: ['/bin/sh', '-c', 'xcode-select -p >/dev/null 2>&1 && exec /usr/bin/python3 -I -c "$PY" "$@"', 'sh', ...pids.map(String)],
116  env: { PY: RUSAGE_PY },
117})
118
119// One nettop sample takes about 5 s, so it runs beside the 2 s sampler, not in it
120export const NET_PROBE = ['/usr/bin/nettop', '-P', '-L', '1', '-x', '-J', 'bytes_in,bytes_out'] as const
121
122export type Counters = Map<number, number[]>
123
124/** "pid read written energyNj" lines: cumulative disk bytes and energy (-1 when unknown). */
125export function parseRusage(stdout: string): Counters {
126  const out: Counters = new Map()
127  for (const line of stdout.trim().split('\n')) {
128    const [pid, ...rest] = line.trim().split(/\s+/).map(Number)
129    if (pid && rest.length === 3) out.set(pid, rest)
130  }
131  return out
132}
133
134/** nettop's "name.pid,bytes_in,bytes_out," lines: cumulative bytes per process. */
135export function parseNettop(stdout: string): Counters {
136  const out: Counters = new Map()
137  for (const line of stdout.split('\n')) {
138    const m = /\.(\d+),(\d+),(\d+),$/.exec(line.trim())
139    if (m) out.set(Number(m[1]), [Number(m[2]), Number(m[3])])
140  }
141  return out
142}
143
144/**
145 * Per-second rates between two counter samples, over the pids in `pids` that
146 * both samples hold (a process that started or ended in between is skipped,
147 * as is a counter that went backwards). Returns the summed rates and each pid's.
148 */
149export function rates(prev: Counters, cur: Counters, seconds: number, pids: Iterable<number>) {
150  const perPid = new Map<number, number[]>()
151  let total: number[] = []
152  for (const pid of pids) {
153    const a = prev.get(pid)
154    const b = cur.get(pid)
155    if (!a || !b || seconds <= 0) continue
156    const r = b.map((v, i) => Math.max(0, v - (a[i] ?? v)) / seconds)
157    perPid.set(pid, r)
158    total = r.map((v, i) => v + (total[i] ?? 0))
159  }
160  return { total, perPid }
161}
162
163export function bytes(n: number): string {
164  const units = ['B', 'KB', 'MB', 'GB']
165  let i = 0
166  while (n >= 1000 && i < units.length - 1) {
167    n /= 1024
168    i++
169  }
170  return `${n >= 100 || i === 0 ? n.toFixed(0) : n.toFixed(1)} ${units[i]}`
171}
172
types/index.d.ts 58 lines
1export type Proc = {
2  pid: number
3  ppid: number
4  depth: number
5  cpu: number
6  rssKb: number
7  elapsed: string
8  name: string
9  /** Average power over the last sample, in watts; null until two samples */
10  watts: number | null
11}
12
13export type Snapshot = {
14  rootPid: number
15  rootName: string
16  cores: number
17  memBytes: number
18  load: number[]
19  cpu: number
20  rssKb: number
21  procs: Proc[]
22}
23
24/** Rates over the last sample, per second, for the whole tree */
25export type Io = {
26  /** Disk bytes read and written per second, and watts; null without rusage */
27  diskRead: number
28  diskWrite: number
29  watts: number
30  /** False before macOS 13, where per-process energy is not reported */
31  hasEnergy: boolean
32  /** Energy since the pane or bar started sampling, in joules */
33  joules: number
34}
35
36export type Net = { bytesIn: number; bytesOut: number }
37
38export type Stats = {
39  snapshot: Snapshot | null
40  error: string | null
41  cpuHistory: number[]
42  ramHistory: number[]
43  /** null until two rusage samples; also null where rusage is unavailable */
44  io: Io | null
45  isRusageAvailable: boolean
46  /** null until two nettop samples */
47  net: Net | null
48  netHistory: number[]
49  diskHistory: number[]
50  energyHistory: number[]
51}
52
53declare module 'claude-code' {
54  interface PluginState {
55    'session-stats': { stats: Stats; isBarOn: boolean }
56  }
57}
58