@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 +2 -1
- package/src/binaries-tab.ts +3 -2
- package/src/pix-command.ts +49 -10
- package/src/sections/pretty.ts +0 -2
- package/src/sysstat.ts +56 -105
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-runtime",
|
|
3
|
-
"version": "0.17.
|
|
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": {
|
package/src/binaries-tab.ts
CHANGED
|
@@ -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 +
|
|
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
|
|
package/src/pix-command.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
}
|
package/src/sections/pretty.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
19
|
-
ram?: number;
|
|
20
|
-
/** GPU busy, 0–100 (Linux amdgpu/i915/xe sysfs only). */
|
|
6
|
+
cpuTemp?: number;
|
|
21
7
|
gpu?: number;
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
78
|
-
const
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
for (const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
}
|