Skip to content

feat(cli): CH 03 Utilities channel + status-line append fix - #102

Merged
distroinfinity merged 7 commits into
mainfrom
organic-bread
Jun 2, 2026
Merged

distroinfinity merged 7 commits into
mainfrom
organic-bread

Conversation

@distroinfinity

@distroinfinity distroinfinity commented Jun 1, 2026 •

Copy link
Copy Markdown
Owner

What

Adds CH 03 — Utilities, a third rotating slot alongside NEWS and MARKETS that surfaces local, real-time dev telemetry, and fixes a status-line regression found along the way.

1. Status-line append fix (bug)

Claude Code's statusLine is a single command slot. We were overwriting it wholesale and ignoring stdin, silently clobbering any custom status line a user already had. Now we stash the existing command (~/.distro/wrapped-statusline.json), run it (same stdin, 800ms timeout), and append our line below it; uninstall restores the original.

2. CH 03 — Utilities channel

The same stdin Claude pipes to the status line is parsed into a local snapshot that feeds a locally-built instrument panel, injected every 3rd slot (news → markets → utils). Local-only — never hits the API, usage/cost stays on device.

Panel (final design):

▍ utils
⎇ branch ↑3 ↓1 │ $29.81 · $0.94/m · ~4h 52m │ Opus 4.8 / high │ ⚠ api degraded
ctx ███░░ 42%   │ cache ████ 99%             │ week █░ 14% · 3d │ 5h █░ 9% · 4h 6m
────────────────────────────────────────────────────────────────────────────────
cpu ███░ 38     │ mem ████ 44                │ disk ███░ 43
  • one shared 4-column grid, fill+track bars aligned across rows, per-metric hues
  • gauges: ctx · cache · weekly · 5-hour (the two Claude limits kept together)
  • context: git (branch + ahead/behind, git-CLI style) · cost · burn · ~time-to-limit · model/effort · api line only on an incident
  • machine: cpu / mem / disk — accurate (sampled CPU, real memory_pressure, disk-used); not load-avg/free-mem junk
  • responsive: scales bar width + degrades gracefully on narrow terminals (gauges always fit)
  • toggle via distro preferences → utilities panel (CLI-local pref)

Accuracy

Every stat fact-checked: ctx/limits/cost/cache/model from Claude's own data, git from real git, api from status.anthropic.com. CPU/mem were fixed (macOS os.freemem is meaningless; load-avg overstates). Anything unreadable is omitted, never faked.

Testing

  • 151 CLI unit tests + monorepo typecheck + lint all green.
  • Real-artifact run: built the release tarball as CI does, installed via install.sh (versioned layout + real better-sqlite3 prebuild + shims), ran the real daemon driven by real hook events — confirmed status-line append, rotation, accurate live stats, no crashes.

Notes

  • Scoped as a local channel — not wired into the server CHANNEL_KEYS/news-selection flow.
  • Out of scope (v1): backend sync / dashboard history, cross-tool aggregation.

Docs: gitbook-docs/cli/utilities-channel.md.

statusline was clobbering the user's existing custom status line —
Claude's statusLine is a single command slot and we overwrote it.
now setStatusLine stashes any pre-existing command, `distro statusline`
runs it (same stdin) and appends our line below, uninstall restores it.

same stdin parse feeds CH 03: a locally-built instrument panel rotating
alongside news/markets showing claude usage (limits/reset, ctx, cost,
cache, burn, time-to-limit), git state, machine vitals, service health.
local-only, every-3rd-slot injection, full vs complement layout, gated
by utilitiesEnabled pref (distro preferences → utilities panel).
@vercel

vercel Bot commented Jun 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
devdrip Ready Ready Preview, Comment Jun 2, 2026 12:28pm

flat dot-separated text → reference-style gauges. context/gauge/machine
rows share one 4-column grid (teal·amber·violet·periwinkle) with fill+track
bars that line up across rows and │ separators that run straight down.

- bars always shown (no more text-only complement); branch keeps name +
  ↑ahead ↓behind vs remote; api line hidden unless degraded/down
- mem bar reddens ≥90%; 5h/7d/ctx redden + ⚠ at warn threshold
- bar width scales with tty width; narrow terminals degrade gracefully
  (full → drop machine → slim context → gauges-only) so core gauges fit
machine stats were wrong on macOS — os.freemem reported ~97% used (it's
meaningless when RAM is used for cache) and load-avg overstated cpu. now:
- cpu: real 250ms sample of cpu times (instantaneous % busy)
- mem: real pressure via memory_pressure (macOS) / MemAvailable (linux);
  omitted, never faked, when unavailable
- disk: % used (100 - free) so a fuller disk reads as a fuller bar
- battery reddens only when LOW (≤20%), not at full charge

panel polish: drop the "distro tv · utils" footer + "· live" suffix, relabel
the weekly gauge "7d" → "week" (it's the limit users hit first), and add a
blank line between the gauge and machine rows for readability.

rotation verified working (news → markets → utils, 1-in-3); the freeze users
saw is the intended idle-pin when no prompt fires for ACTIVE_WINDOW_MS.
… rule divider

- git: show just branch + ↑ahead ↓behind vs remote (git-CLI style); drop the
  uncommitted-files count
- machine: remove battery (not useful in a terminal) → cpu / mem / disk
- gauge order ctx · cache · week · 5h so the weekly + 5-hour limits sit together
- replace the gauge/machine separator with a dim horizontal rule (a blank line
  was being collapsed by Claude's statusLine)
swap the dim horizontal rule between the gauge and machine rows for a blank
gap. a truly-empty line gets collapsed by Claude's statusLine, so the row is
non-breaking spaces — it renders blank but carries width, so it survives.
…e line

a non-breaking-space "blank" row made Claude's statusLine truncate every row
after it (the machine row vanished). a plain empty line just collapses to no
gap. so revert to a thin divider, dimmed (60,64,74) to read as breathing room
rather than a hard line — the lightest separator that renders the machine row.
@distroinfinity distroinfinity changed the title feat(cli): CH 03 utilities channel + status-line append fix feat(cli): CH 03 Utilities channel + status-line append fix Jun 2, 2026
@distroinfinity

Copy link
Copy Markdown
Owner Author

hey @claude
review this PR thoroughly:
•⁠ ⁠understand the feature that we are targeting then review the approach and implementation.
•⁠ ⁠review architectural level changes.
•⁠ ⁠review for bugs, unhandled errors, silent failure or dead code.
•⁠ ⁠poor coding practices, anti patterns, code smells that can be refactored or improved.
•⁠ ⁠if there are dead or obsolete code which can now be refactored or removed point that out

@claude

claude Bot commented Jun 2, 2026 •

Copy link
Copy Markdown

Claude finished @distroinfinity's task in 4m 1s —— View job


PR Review: CH 03 Utilities channel + status-line append fix

Tasks

  • Understand the feature and approach
  • Review architectural changes
  • Review changed files for bugs, errors, silent failures
  • Review for anti-patterns, code smells
  • Check for dead/obsolete code
  • Post final review

Feature Summary

The PR ships two things: a status-line wrap fix (stash + chain the user's existing command rather than clobbering it) and CH 03 Utilities — a locally-built instrument panel injected every 3rd slot. The panel pulls from three sources: Claude's own stdin telemetry, local probes (git, CPU/mem/disk, Anthropic status page), and a rolling history file for burn-rate derivation. Everything stays on-device; no API calls from the utility path. Architecture is clean overall — the approach is sound. Issues below are real but scoped.


Bugs

1. Double (triple) snapshot reads on every build() call
utility-slot.ts:250–251 calls readUsageSnapshot() (which calls readSnapshot() internally), then aiFrom() at line 260 calls deriveUsage() at line 227 which calls readSnapshot() again — a second synchronous file read of the same file per rotation. build() is called on every slot pick.

// utility-slot.ts:248-260
build() {
  const t = now()
  const usage = readUsageSnapshot(t, UTILITY_SNAPSHOT_STALE_MS)   // read #1
  ...
  const ai = aiFrom(usage)   // → deriveUsage() → readSnapshot()  // read #2

Fix: pass the snapshot into deriveUsage or merge the derive logic into readUsageSnapshot. Fix this →


2. cachePct formula is likely wrong
claude-usage.ts:105–110:

const cacheRead   = ...cache_read_input_tokens
const cacheCreate = ...cache_creation_input_tokens
const inTok       = ...input_tokens
const cacheDenom  = cacheRead + cacheCreate + inTok
const cachePct    = cacheDenom > 0 ? Math.round((cacheRead * 100) / cacheDenom) : undefined

cache_creation_input_tokens are tokens being written to the cache for the first time (they cost 1.25× the base rate). Including them in the denominator but not the numerator deflates cachePct on sessions that are actively building the cache. A standard "cache hit rate" is cacheRead / (cacheRead + inTok). As written, a session that just created a 100k-token cache and read nothing from it would show 0 / (0 + 100k + 0) = 0% which is technically correct, but the next turn would show 100k / (100k + 0 + 0) = 100%, which is what users expect. The issue is sessions mid-cache-build show misleading dips. Fix this →


3. Degradation fallback loop — chosen = r in the non-break path is a latent bug
render-line.ts:402–411:

let chosen = build(attempts[attempts.length - 1] as string[][])  // pre-seed with narrowest
for (const rows of attempts) {
  const r = build(rows)
  if (r.max <= W) {
    chosen = r
    break
  }
  chosen = r   // ← overwrites the pre-seeded fallback every iteration
}

This currently works because attempts[attempts.length - 1] is [gau], and the loop's last element is also [gau] — so if nothing fits, we always land on [gau] regardless. But the chosen = r in the non-break path is misleading (it sets chosen to the oversized layout mid-loop) and would break if the array order changes or a new attempt is appended after [gau]. The intent should be explicit:

let chosen = build(attempts[attempts.length - 1] as string[][])
for (const rows of attempts) {
  const r = build(rows)
  if (r.max <= W) { chosen = r; break }
}

Fix this →


Silent Failures

4. Background probe errors are entirely swallowed
utility-slot.ts:208–216:

run()
  .then((v) => { cell.value = v; cell.at = now() })
  .catch(() => {})   // ← no log, no metric
  .finally(() => { cell.inFlight = false })

When a probe (git, machine, health) fails, we get no signal. In a long-running daemon this makes diagnosing "why is the utility panel empty" impossible. The daemon already has a log dep available in the orchestrator — consider threading it into the provider or at minimum changing .catch(() => {}) to .catch((e) => { /* debug log */ }). The hard requirement that probes never throw is correct; the silence isn't.


5. spawnSync for wrapped status-line: status and error not checked
statusline.ts:44–55:

const res = spawnSync("sh", ["-c", wrapped.command], {
  input, encoding: "utf8", timeout: 800, maxBuffer: 1024 * 1024,
})
const out = (res.stdout ?? "").replace(/\n+$/, "")
if (out.length > 0) parts.push(out)

spawnSync sets res.error on spawn failure (command not found, timeout) and res.status on non-zero exit. Neither is checked. If the wrapped command times out, res.error is set and res.stdout may be "" — so we fall through silently, which is the right behavior. But if the wrapped command writes to stderr and the user expects to see it, it's quietly dropped. This is intentional by design but worth a comment since it will confuse anyone debugging a broken wrapped command.


Dead / Unused Code

6. UtilityAi.burnTokPerMin and UtilityAi.compactAtPct are never populated
UtilityPayload.ts:26, 19:

burnTokPerMin?: number    // defined but never computed in deriveUsage()
compactAtPct?: number     // defined but never parsed from stdin or set anywhere

Neither field is set anywhere — deriveUsage() only computes burnUsdPerMin and timeToLimitMin, and parseStatuslineInput() has no path for compactAtPct. If these are placeholders for v2, add a // TODO: comment. Otherwise delete them to avoid confusion. Fix this →


Code Smells / Anti-patterns

7. Reference equality check to inject a visual separator is fragile
render-line.ts:381:

if (r === mac && max > 0) {
  lines.push(`${LEFT_PAD}${rgb("\u2500".repeat(ruleW), ...)}`)
}

This uses object identity (r === mac) to detect "we're about to render the machine row". It works as long as mac in attempts is the same array reference, but is brittle to any refactor that copies/spreads the row arrays. A cleaner pattern would be to mark rows with a sentinel or pass the separator decision as metadata alongside the rows.


8. deriveUsage parameter semantics are misleading
claude-usage.ts:179–182:

export function deriveUsage(now: number, staleMs: number): UsageDerived {
  ...
  const hist = snap.history.filter((s) => now - s.ts <= Math.max(staleMs, 10 * 60_000))

The staleMs parameter implies it controls the derivation window, but Math.max(staleMs, 10 * 60_000) means the window is always at least 10 minutes regardless of what you pass. Since UTILITY_SNAPSHOT_STALE_MS = 5 * 60_000, the argument has no effect on the filter at all — it always uses 10 min. Either remove the staleMs param and hardcode 10 * 60_000 as BURN_RATE_WINDOW_MS, or honour the param without the silent clamp.


9. fmtMins defined below its first call site
render-line.ts:261 calls fmtMins but it's declared at line 417. Function declarations hoist in JS, so no runtime issue, but it makes the file harder to read. Minor — consider moving fmtMins above renderUtilityPanel.


Architecture Notes (no action needed, FYI)

  • livePrefs / utilitiesEnabled config hot-reload works correctly — the watchFile callback updates livePrefs and calls orchestrator.updatePreferences(), so toggling utilities via distro preferences propagates within 1s without a daemon restart. Good.
  • pickCount shared across all tty sessions — intentional (prevents double-rate on multi-terminal setups) and documented in the comment. Fine.
  • utility kind excluded from writeNowPlaying — correct for a local-only channel.
  • Atomic writes (tmp + rename) in writeSnapshot and writeWrappedStatusLine — good pattern, consistent across the codebase.
  • probeGit handles no-upstream branches correctly — rev-list @{upstream}...HEAD returns null and ahead/behind are simply omitted. Clean.

Summary

Severity Issue File
🔴 Bug Double file read on every build() utility-slot.ts:227
🔴 Bug cachePct includes write tokens in denominator claude-usage.ts:109
🟡 Latent bug Fallback loop chosen = r in non-break path render-line.ts:403
🟡 Silent failure Probe errors silently swallowed utility-slot.ts:213
🟡 Dead code burnTokPerMin, compactAtPct never populated UtilityPayload.ts:19,26
🟠 Code smell Reference equality for separator insertion render-line.ts:381
🟠 Code smell staleMs param in deriveUsage has no effect claude-usage.ts:182
ℹ️ Style fmtMins declared after first call render-line.ts:417

The status-line wrap logic, the probe/cache architecture, and the progressive degradation rendering are all solid. The cachePct formula and the double-read are the two issues worth fixing before merge.

… smells

- claude-usage: single snapshot read per rotation via loadUsage() (was reading
  the same file twice — readUsageSnapshot + deriveUsage); cache-hit-rate now
  excludes cache_creation tokens (writes, not reads) from the denominator;
  burn-rate window is an explicit BURN_RATE_WINDOW_MS const (the old staleMs
  arg was silently clamped and had no effect)
- render-line: replace fragile `r === mac` identity check with a GridRow
  { cells, sepBefore } shape; drop the misleading `chosen = r` fallback in the
  non-break path (pre-seed handles it)
- utility-slot: background probe failures now reported via an optional debug
  logger (daemon threads log.debug in) instead of a silent .catch(() => {})
- statusline: skip wrapped-command output when spawnSync sets res.error
  (timeout / spawn failure) so we never emit a partial line
- shared: drop never-populated UtilityAi fields (burnTokPerMin, compactAtPct)
@distroinfinity

Copy link
Copy Markdown
Owner Author

Thanks for the thorough review — all findings addressed in 7d00c87. Per item:

1. 🔴 Double snapshot read per build() — Fixed. Replaced readUsageSnapshot() + deriveUsage() (which each did their own readSnapshot()) with a single loadUsage(now, staleMs) that reads the file once and returns { latest, derived }. build() now does one read per rotation.

2. 🔴 cachePct formula — Fixed. cache_creation_input_tokens are writes, not reads, so they're excluded from the denominator: cacheRead / (cacheRead + inTok). Added a test: a mid-cache-build turn (reads=0, creation=100k) now returns undefined instead of a misleading 0%, and steady-state (90k read / 10k creation / 10k input) returns 90%.

3. 🟡 Fallback loop chosen = r in non-break path — Fixed. Removed the chosen = r from the non-break path; the pre-seeded narrowest layout is only replaced when an attempt actually fits.

4. 🟡 Probe errors swallowed — Fixed. createUtilityProvider takes an optional log dep; the daemon threads log.debug in, and the refresh() catch now reports utility probe failed: <label> with the error message. Probes still never throw into the render path.

5. 🟡 spawnSync error/status unchecked — Fixed. We now skip the wrapped output when res.error is set (timeout / spawn failure), so a partial stdout chunk can never be emitted as a half-rendered line. stderr being dropped is intentional and now documented in a comment.

6. 🟡 Dead fields burnTokPerMin / compactAtPct — Removed both from UtilityAi (no v2 plan; nothing populated them).

7. 🟠 r === mac reference equality — Fixed. Rows are now a GridRow { cells: string[]; sepBefore?: boolean }; the divider is driven by r.sepBefore, so it survives any row copy/spread refactor.

8. 🟠 deriveUsage staleMs no-op clamp — Fixed. The window is now an explicit BURN_RATE_WINDOW_MS = 10 * 60_000 const used directly; the misleading staleMs param is gone.

9. ℹ️ fmtMins declared after first call — Left as-is intentionally: it's a hoisted function declaration (valid, zero runtime impact) and moving it adds churn without changing behavior. Happy to relocate if you'd prefer it for readability.

Verification: 151 unit tests + monorepo typecheck + lint all green; panel re-rendered (gauges + divider + machine row) and the new cache-rate test added then removed (per our no-lingering-throwaway-tests convention).

@distroinfinity
distroinfinity merged commit 26f80b1 into main Jun 2, 2026
5 checks passed
@distroinfinity
distroinfinity deleted the organic-bread branch September 19, 2026 20:46

This branch was successfully deployed

1 active deployment
Preview — 7d00c87f Deployed Jun 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant