@xynogen/pix-runtime 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-runtime",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "Pix shared runtime — versioned pix.json config, atomic persistence, typed change events",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -69,6 +69,7 @@
69
69
  "@ast-grep/napi": "^0.45.0"
70
70
  },
71
71
  "dependencies": {
72
+ "systeminformation": "5.33.19",
72
73
  "xxhash-wasm": "1.1.0"
73
74
  },
74
75
  "peerDependenciesMeta": {
@@ -245,9 +245,10 @@ export function createBinariesTab(opts: BinariesTabOptions) {
245
245
 
246
246
  view(width: number): TabView {
247
247
  const nameW = Math.max(6, ...rows.map((r) => r.name.length));
248
+ const iconW = Math.max(2, ...Object.values(STATE_ICON).map((key) => visibleWidth(icon(key))));
248
249
  // Frame border + padding take 4 columns; every row must fit on one line.
249
250
  const inner = Math.max(20, width - 4);
250
- const lead = 2 + 2 + nameW + 2; // cursor, glyph, name, gap
251
+ const lead = 2 + iconW + 1 + nameW + 2; // cursor, glyph, name, gap
251
252
  const body: string[] = [];
252
253
  let selectedBodyLine: number | undefined;
253
254
  let otherHeader = false;
@@ -294,7 +295,7 @@ export function createBinariesTab(opts: BinariesTabOptions) {
294
295
  detail = theme.fg("muted", `not used on this OS · ${row.hint}`);
295
296
  }
296
297
  if (sel) selectedBodyLine = body.length;
297
- const line = `${cursor} ${glyph} ${name} ${detail}`;
298
+ const line = `${cursor} ${glyph}${" ".repeat(iconW - visibleWidth(glyph))} ${name} ${detail}`;
298
299
  body.push(visibleWidth(line) > inner ? truncateToWidth(line, inner, "…") : line);
299
300
  }
300
301
 
@@ -213,7 +213,27 @@ const SETTINGS: SettingRow<unknown>[] = [
213
213
  const part = key as keyof typeof prettySection.defaults.footer;
214
214
  return row({
215
215
  section: "Footer",
216
- label: part === "wrap" ? "wrap line" : part,
216
+ get label() {
217
+ const key = {
218
+ mode: "status.active",
219
+ cwd: "cwd",
220
+ git: "git.unstaged",
221
+ context: "tokens",
222
+ model: "model",
223
+ thinking: "agent",
224
+ score: "score",
225
+ statuses: "status.running",
226
+ tokens: "tokens",
227
+ cost: "data.float",
228
+ tps: "score",
229
+ system: "sys.cpu",
230
+ clock: "clock",
231
+ wrap: "turns",
232
+ } as const;
233
+ const glyph = icon(key[part]);
234
+ const iconW = Math.max(2, ...Object.values(key).map((key) => visibleWidth(icon(key))));
235
+ return `${glyph}${" ".repeat(iconW - visibleWidth(glyph))} ${part === "wrap" ? "wrap line" : part}`;
236
+ },
217
237
  handle: prettySection,
218
238
  values: part === "wrap" ? ["on", "off"] : ["show", "hide"],
219
239
  read: (v) =>
@@ -457,14 +477,20 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
457
477
  });
458
478
  return binaries;
459
479
  };
460
- const tabBar = () =>
461
- tabs
480
+ const tabBar = (width: number) => {
481
+ const full = tabs
462
482
  .map((name) =>
463
483
  name === tab
464
484
  ? theme.fg("accent", theme.bold(` ${name} `))
465
485
  : theme.fg("muted", ` ${name} `),
466
486
  )
467
487
  .join(theme.fg("muted", "│"));
488
+ if (visibleWidth(full) <= width) return full;
489
+ return (
490
+ theme.fg("accent", theme.bold(` ${tab} `)) +
491
+ theme.fg("muted", `${tabs.indexOf(tab) + 1}/${tabs.length}`)
492
+ );
493
+ };
468
494
  const switchTab = (direction: -1 | 1) => {
469
495
  const next = (tabs.indexOf(tab) + direction + tabs.length) % tabs.length;
470
496
  tab = tabs[next] ?? "Settings";
@@ -548,7 +574,7 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
548
574
  const hints = view.footer.map(([key, action]) => guide(key, action)).join(guideSep);
549
575
  const result = frameModal({
550
576
  ...frame,
551
- header: [tabBar(), "", ...view.header],
577
+ header: [tabBar(Math.max(1, width - 4)), "", ...view.header],
552
578
  body: view.body,
553
579
  footer: [
554
580
  "",
@@ -573,19 +599,26 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
573
599
  lines = embedded.render(width);
574
600
  }
575
601
  const panelWidth = visibleWidth(lines[0] ?? "");
576
- lines.splice(1, 0, ...frameRows(panelWidth, [tabBar()], frame).slice(1, -1));
602
+ lines.splice(
603
+ 1,
604
+ 0,
605
+ ...frameRows(panelWidth, [tabBar(Math.max(1, panelWidth - 4))], frame).slice(
606
+ 1,
607
+ -1,
608
+ ),
609
+ );
577
610
  return lines;
578
611
  }
579
612
  return frameModal({
580
613
  ...frame,
581
- header: [tabBar(), ""],
614
+ header: [tabBar(Math.max(1, width - 4)), ""],
582
615
  body: [theme.fg("dim", ` Loading ${tab} settings…`)],
583
616
  footer: ["", tabGuide + guideSep + guide("esc", "close")],
584
617
  bodyOffset: 0,
585
618
  }).lines;
586
619
  }
587
620
  const settings = rows();
588
- const labelW = Math.max(...settings.map((r) => r.label.length));
621
+ const labelW = Math.max(...settings.map((r) => visibleWidth(r.label)));
589
622
  const body: string[] = [];
590
623
  const settingBodyLines: number[] = [];
591
624
  let lastSection = "";
@@ -599,7 +632,10 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
599
632
  }
600
633
  const sel = i === selected;
601
634
  const cursor = sel ? theme.fg("accent", "→") : " ";
602
- const label = theme.fg(sel ? "accent" : "text", row.label.padEnd(labelW));
635
+ const label = theme.fg(
636
+ sel ? "accent" : "text",
637
+ row.label + " ".repeat(labelW - visibleWidth(row.label)),
638
+ );
603
639
  const value = isThemeRow(row) ? row.current() : row.read(runtime.get(row.handle));
604
640
  const isDefault = isThemeRow(row) || value === row.read(row.handle.defaults);
605
641
  settingBodyLines[i] = body.length;
@@ -607,7 +643,7 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
607
643
  }
608
644
  const result = frameModal({
609
645
  ...frame,
610
- header: [tabBar(), ""],
646
+ header: [tabBar(Math.max(1, width - 4)), ""],
611
647
  body,
612
648
  footer: [
613
649
  "",
@@ -782,7 +818,10 @@ function frameModal(opts: RuntimeModalOptions): RuntimeModalResult {
782
818
  const maxBodyOffset = Math.max(0, body.length - visibleBodyLines);
783
819
  let offset = Math.min(maxBodyOffset, Math.max(0, opts.bodyOffset));
784
820
  if (opts.selectedBodyLine !== undefined) {
785
- const selected = Math.min(body.length - 1, Math.max(0, opts.selectedBodyLine));
821
+ const selected = Math.min(
822
+ body.length - 1,
823
+ fitLines(opts.body.slice(0, Math.max(0, opts.selectedBodyLine)), inner).length,
824
+ );
786
825
  if (selected < offset) offset = selected;
787
826
  else if (selected >= offset + visibleBodyLines) offset = selected - visibleBodyLines + 1;
788
827
  }
@@ -26,7 +26,6 @@ export interface PrettyConfig {
26
26
  | "context"
27
27
  | "model"
28
28
  | "thinking"
29
- | "price"
30
29
  | "score"
31
30
  | "statuses"
32
31
  | "tokens"
@@ -68,7 +67,6 @@ const DEFAULTS: Readonly<PrettyConfig> = {
68
67
  context: true,
69
68
  model: true,
70
69
  thinking: true,
71
- price: true,
72
70
  score: true,
73
71
  statuses: true,
74
72
  tokens: true,
package/src/sysstat.ts CHANGED
@@ -1,118 +1,69 @@
1
- /**
2
- * sysstat.ts — cheap host load sample for status bars: CPU %, RAM %, GPU %, CPU temperature.
3
- *
4
- * CPU and RAM come from `node:os`, so they work on every OS. GPU busy % and temperature
5
- * are read from Linux sysfs (`/sys/class/drm`, `/sys/class/hwmon`, `/sys/class/thermal`);
6
- * elsewhere, or when the driver exposes nothing, those fields are undefined.
7
- * ponytail: no NVIDIA support (its driver has no sysfs busy file). Add an `nvidia-smi`
8
- * catalog binary and a slow poll here if that is needed.
9
- */
10
-
11
- import { readdirSync, readFileSync } from "node:fs";
12
- import { cpus, freemem, totalmem } from "node:os";
13
- import { join } from "node:path";
1
+ import { promises as fs } from "node:fs";
2
+ import si from "systeminformation";
14
3
 
15
4
  export interface SystemStats {
16
- /** Busy CPU since the previous sample, 0–100. Undefined on the first sample. */
17
5
  cpu?: number;
18
- /** Used memory (total − available), 0–100. */
19
- ram?: number;
20
- /** GPU busy, 0–100 (Linux amdgpu/i915/xe sysfs only). */
6
+ cpuTemp?: number;
21
7
  gpu?: number;
22
- /** CPU package temperature in °C (Linux hwmon/thermal only). */
23
- temp?: number;
24
- }
25
-
26
- /** Opaque CPU counter snapshot; pass the previous one to get a CPU percent. */
27
- export type CpuTimes = { busy: number; total: number };
28
-
29
- export function cpuTimes(): CpuTimes {
30
- let busy = 0;
31
- let total = 0;
32
- for (const { times } of cpus()) {
33
- const sum = times.user + times.nice + times.sys + times.idle + times.irq;
34
- total += sum;
35
- busy += sum - times.idle;
36
- }
37
- return { busy, total };
38
- }
39
-
40
- /** Busy share between two snapshots, 0–100, or undefined when no time passed. */
41
- export function cpuPercent(prev: CpuTimes, next: CpuTimes): number | undefined {
42
- const total = next.total - prev.total;
43
- if (total <= 0) return undefined;
44
- return clampPct(((next.busy - prev.busy) / total) * 100);
45
- }
46
-
47
- const clampPct = (n: number) => Math.min(100, Math.max(0, Math.round(n)));
48
-
49
- function readNumber(path: string): number | undefined {
50
- try {
51
- const n = Number(readFileSync(path, "utf8").trim());
52
- return Number.isFinite(n) ? n : undefined;
53
- } catch {
54
- return undefined;
55
- }
56
- }
57
-
58
- function list(dir: string): string[] {
59
- try {
60
- return readdirSync(dir);
61
- } catch {
62
- return [];
63
- }
64
- }
65
-
66
- /** Highest `gpu_busy_percent` across DRM cards (a laptop may report an idle iGPU too). */
67
- export function readGpuBusy(sys = "/sys"): number | undefined {
68
- let best: number | undefined;
69
- for (const card of list(join(sys, "class/drm"))) {
70
- if (!/^card\d+$/.test(card)) continue;
71
- const n = readNumber(join(sys, "class/drm", card, "device/gpu_busy_percent"));
72
- if (n !== undefined) best = Math.max(best ?? 0, clampPct(n));
73
- }
74
- return best;
8
+ gpuTemp?: number;
9
+ ram?: number;
75
10
  }
76
11
 
77
- // CPU sensor drivers, most specific first. coretemp = Intel, k10temp/zenpower = AMD.
78
- const CPU_HWMON = ["k10temp", "zenpower", "coretemp", "cpu_thermal", "acpitz"];
12
+ /** Missing sensors, sentinel values and failed probes stay unavailable, never become zero. */
13
+ const reading = (value: unknown, max: number): number | undefined =>
14
+ typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= max
15
+ ? Math.round(value)
16
+ : undefined;
79
17
 
80
- /** CPU temperature in °C from hwmon (known CPU drivers), then thermal_zone0. */
81
- export function readCpuTemp(sys = "/sys"): number | undefined {
82
- const hwmon = join(sys, "class/hwmon");
83
- const byName = new Map<string, string>();
84
- for (const dir of list(hwmon)) {
85
- try {
86
- byName.set(readFileSync(join(hwmon, dir, "name"), "utf8").trim(), dir);
87
- } catch {}
18
+ async function readAmdStats(): Promise<SystemStats> {
19
+ const stats: SystemStats = {};
20
+ const read = (path: string) => fs.readFile(path, "utf8").catch(() => "");
21
+ const cards = await fs.readdir("/sys/class/drm").catch(() => []);
22
+ for (const card of cards.filter((name) => /^card\d+$/.test(name))) {
23
+ const device = `/sys/class/drm/${card}/device`;
24
+ if ((await read(`${device}/vendor`)).trim() !== "0x1002") continue;
25
+ const busy = (await read(`${device}/gpu_busy_percent`)).trim();
26
+ const gpu = busy ? reading(Number(busy), 100) : undefined;
27
+ if (gpu !== undefined) stats.gpu = Math.max(stats.gpu ?? 0, gpu);
28
+ const sensors = await fs.readdir(`${device}/hwmon`).catch(() => []);
29
+ for (const sensor of sensors) {
30
+ const raw = (await read(`${device}/hwmon/${sensor}/temp1_input`)).trim();
31
+ const temperature = raw ? reading(Number(raw) / 1000, 150) : undefined;
32
+ if (temperature !== undefined) stats.gpuTemp = Math.max(stats.gpuTemp ?? 0, temperature);
33
+ }
88
34
  }
89
- for (const name of CPU_HWMON) {
90
- const dir = byName.get(name);
91
- const milli = dir && readNumber(join(hwmon, dir, "temp1_input"));
92
- if (milli) return Math.round(milli / 1000);
93
- }
94
- const zone = readNumber(join(sys, "class/thermal/thermal_zone0/temp"));
95
- return zone ? Math.round(zone / 1000) : undefined;
35
+ return stats;
96
36
  }
97
37
 
98
- /**
99
- * Take one sample. Keep the returned `times` and pass it back next call so CPU % covers
100
- * the interval between calls. Synchronous and cheap (a few small sysfs reads).
101
- */
102
- export function readSystemStats(
103
- prev?: CpuTimes,
104
- sys = "/sys",
105
- ): { stats: SystemStats; times: CpuTimes } {
106
- const times = cpuTimes();
107
- const total = totalmem();
108
- const linux = process.platform === "linux";
38
+ /** Read each source independently so an unsupported sensor cannot discard other readings. */
39
+ export async function readSystemStats(): Promise<SystemStats> {
40
+ const [cpu, temperature, graphics, memory] = await Promise.allSettled([
41
+ si.currentLoad(),
42
+ si.cpuTemperature(),
43
+ si.graphics(),
44
+ si.mem(),
45
+ ]);
46
+ const controllers = graphics.status === "fulfilled" ? graphics.value.controllers : [];
47
+ const highest = (key: "utilizationGpu" | "temperatureGpu", max: number) => {
48
+ const values = controllers
49
+ .map((controller) => reading(controller[key], max))
50
+ .filter((value): value is number => value !== undefined);
51
+ return values.length ? Math.max(...values) : undefined;
52
+ };
53
+ const gpu = highest("utilizationGpu", 100);
54
+ const gpuTemp = highest("temperatureGpu", 150);
55
+ const amd =
56
+ process.platform === "linux" && (gpu === undefined || gpuTemp === undefined)
57
+ ? await readAmdStats()
58
+ : {};
109
59
  return {
110
- times,
111
- stats: {
112
- cpu: prev ? cpuPercent(prev, times) : undefined,
113
- ram: total > 0 ? clampPct(((total - freemem()) / total) * 100) : undefined,
114
- gpu: linux ? readGpuBusy(sys) : undefined,
115
- temp: linux ? readCpuTemp(sys) : undefined,
116
- },
60
+ cpu: cpu.status === "fulfilled" ? reading(cpu.value.currentLoad, 100) : undefined,
61
+ cpuTemp: temperature.status === "fulfilled" ? reading(temperature.value.main, 150) : undefined,
62
+ gpu: gpu ?? amd.gpu,
63
+ gpuTemp: gpuTemp ?? amd.gpuTemp,
64
+ ram:
65
+ memory.status === "fulfilled" && memory.value.total > 0
66
+ ? reading(((memory.value.total - memory.value.available) / memory.value.total) * 100, 100)
67
+ : undefined,
117
68
  };
118
69
  }