feat(tui): add vitest kill + memory-pressure auto-kill to Utilities

[k] kills all running vitest processes via pgrep -f. [v] toggles a
sampler-driven guard that SIGKILLs vitest when system memory exceeds
90%, throttled to once per 30s. Auto-kill defaults ON.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-25 07:45:36 -07:00
parent d281cb2c1e
commit 43d630114b
5 changed files with 148 additions and 16 deletions

View File

@@ -589,11 +589,14 @@ function ExpandedLog({ entry, index, total }: { entry: LogEntry; index: number;
// ── Utilities panel ───────────────────────────────────────────────────────────
function UtilitiesPanel({ isFocused }: { isFocused: boolean }) {
const actions = [
function UtilitiesPanel({ state, isFocused }: { state: DashboardState; isFocused: boolean }) {
const autoKill = state.autoKillVitestOnPressure;
const actions: Array<{ key: string; label: string }> = [
{ key: "r", label: "Refresh Stats" },
{ key: "c", label: "Clear Logs" },
{ key: "t", label: "Toggle Engine Pause" },
{ key: "k", label: "Kill Vitest Processes" },
{ key: "v", label: `Auto-Kill Vitest >90% Mem: ${autoKill ? "ON" : "OFF"}` },
{ key: "?", label: "Help" },
];
return (
@@ -628,6 +631,8 @@ function HelpOverlay() {
["[←] / [p]", "Previous panel (Main)"],
["[r]", "Refresh stats (Utilities)"],
["[c]", "Clear logs (Utilities)"],
["[k]", "Kill all vitest processes (Utilities)"],
["[v]", "Toggle auto-kill vitest >90% mem (Utilities)"],
["[↑/↓/k/j]", "Navigate list / log entries"],
["[Home / G]", "First / last log entry (Logs)"],
["[Enter/Space]", "Expand log entry (Logs)"],
@@ -707,7 +712,7 @@ function StatusModeGrid({
/>
<Box flexDirection="row" overflow="hidden">
<Box flexDirection="column" flexGrow={1} overflow="hidden">
<UtilitiesPanel isFocused={focused === "utilities"} />
<UtilitiesPanel state={state} isFocused={focused === "utilities"} />
</Box>
<Box flexDirection="column" flexGrow={1} overflow="hidden">
<SettingsPanel state={state} isFocused={focused === "settings"} />
@@ -736,7 +741,7 @@ function StatusModeSingle({
switch (focused) {
case "system": return <SystemPanel state={state} isFocused />;
case "logs": return <LogsPanel state={state} isFocused availableRows={Math.max(4, (process.stdout.rows ?? 24) - 8)} />;
case "utilities": return <UtilitiesPanel isFocused />;
case "utilities": return <UtilitiesPanel state={state} isFocused />;
case "stats": return <StatsPanel state={state} isFocused />;
case "settings": return <SettingsPanel state={state} isFocused />;
}
@@ -764,7 +769,7 @@ function StatusBar({ state, controller: _controller }: { state: DashboardState;
if (activeSection === "logs") {
hotkeys.push("↑↓ navigate", "w wrap", "f filter", "Enter expand");
} else if (activeSection === "utilities") {
hotkeys.push("r refresh", "c clear logs", "t toggle pause");
hotkeys.push("r refresh", "c clear logs", "t toggle pause", "k kill vitest", "v auto-kill");
} else {
hotkeys.push("Tab cycle panel", "1-5 jump");
}

View File

@@ -50,6 +50,12 @@ export class DashboardTUI {
logsViewportStart = 0;
loadingStatus = "Starting…";
mode: "status" | "interactive" = "status";
// When true, sampleSystemStats() kills any running vitest processes if
// system memory usage crosses 90%. Toggled by [v] in the Utilities panel.
autoKillVitestOnPressure = true;
// Throttle so we don't spam kills while the sampler keeps firing during
// sustained pressure (sampler runs every 2s).
private lastAutoKillAt = 0;
interactiveData: InteractiveData | null = null;
interactiveView: InteractiveView = "board";
@@ -110,6 +116,7 @@ export class DashboardTUI {
mode: this.mode,
interactiveData: this.interactiveData,
interactiveView: this.interactiveView,
autoKillVitestOnPressure: this.autoKillVitestOnPressure,
};
return this.cachedSnapshot;
}
@@ -195,6 +202,68 @@ export class DashboardTUI {
nodeVersion: process.version,
platform: `${process.platform}/${process.arch}`,
});
if (this.autoKillVitestOnPressure) {
const total = os.totalmem();
const free = os.freemem();
if (total > 0) {
const usedRatio = (total - free) / total;
// 30s minimum gap between auto-kills — vitest restart and OS reclaim
// both take a few seconds; firing every 2s would flap.
if (usedRatio > 0.9 && now - this.lastAutoKillAt > 30_000) {
this.lastAutoKillAt = now;
const result = this.killVitestProcesses();
if (result.killed > 0) {
this.warn(
`Auto-killed ${result.killed} vitest process${result.killed === 1 ? "" : "es"} (system memory at ${Math.round(usedRatio * 100)}%)`,
"memory-guard",
);
}
}
}
}
}
/**
* Find and SIGKILL any running vitest processes, excluding this dashboard
* itself. Returns a count of pids signalled (best-effort — a pid may be
* gone by the time we send the signal).
*/
killVitestProcesses(): { killed: number; pids: number[] } {
const selfPid = process.pid;
let pids: number[] = [];
try {
// pgrep -f matches against the full command line. -a would include the
// command, but we only need pids. macOS and Linux both support -f.
const out = execSync("pgrep -f vitest", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
pids = out
.split("\n")
.map((s) => Number.parseInt(s.trim(), 10))
.filter((n) => Number.isFinite(n) && n > 0 && n !== selfPid);
} catch {
// pgrep exits non-zero when no matches — treat as "nothing to kill".
return { killed: 0, pids: [] };
}
let killed = 0;
for (const pid of pids) {
try {
process.kill(pid, "SIGKILL");
killed += 1;
} catch {
// Process already exited or we lack permission — skip.
}
}
return { killed, pids };
}
toggleAutoKillVitest(): boolean {
this.autoKillVitestOnPressure = !this.autoKillVitestOnPressure;
if (!this.autoKillVitestOnPressure) {
this.lastAutoKillAt = 0;
}
this.notify();
return this.autoKillVitestOnPressure;
}
setSettings(settings: SettingsValues): void {
@@ -335,6 +404,26 @@ export class DashboardTUI {
this.setSettings(newSettings);
}
break;
case "k": {
const result = this.killVitestProcesses();
if (result.killed === 0) {
this.log("No vitest processes found.", "kill-vitest");
} else {
this.warn(
`Killed ${result.killed} vitest process${result.killed === 1 ? "" : "es"}: ${result.pids.join(", ")}`,
"kill-vitest",
);
}
break;
}
case "v": {
const enabled = this.toggleAutoKillVitest();
this.log(
`Auto-kill vitest on memory pressure (>90%): ${enabled ? "ON" : "OFF"}`,
"memory-guard",
);
break;
}
}
}

View File

@@ -279,6 +279,7 @@ export interface DashboardState {
mode: AppMode;
interactiveData: InteractiveData | null;
interactiveView: InteractiveView;
autoKillVitestOnPressure: boolean;
}
export const SECTION_ORDER: SectionId[] = ["system", "logs", "utilities", "stats", "settings"];
@@ -302,5 +303,6 @@ export function createInitialState(): DashboardState {
mode: "status",
interactiveData: null,
interactiveView: "board",
autoKillVitestOnPressure: true,
};
}