Release grok-build-hud v0.3.0: Claude-HUD-style same-window status for Grok Build

Multi-line tmux strip (context, quota, tools, todos, git), theme sync with
Grok UI, full/essential/minimal presets, one-shot installer, EN+ZH docs,
and 40 unit tests.
This commit is contained in:
2026-07-17 15:45:25 +08:00
commit bb6a1321fb
56 changed files with 6334 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { parseUpdatesLines, formatToolLine } from "../src/activity.js";
// dist/tests -> package root is ../..
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const fixtureUpdates = path.join(root, "fixtures", "session", "updates.jsonl");
describe("activity", () => {
it("parses fixture updates into tool summary including read_file", () => {
const lines = fs.readFileSync(fixtureUpdates, "utf8").split(/\r?\n/);
const { tools, agents } = parseUpdatesLines(lines);
assert.ok(tools.length > 0, "expected tools from fixture");
const names = tools.map((t) => t.name);
assert.ok(
names.includes("read_file") || names.some((n) => n.includes("read")),
`expected read_file in ${names.join(",")}`,
);
// call-4 is running (no completed status)
const running = tools.filter((t) => t.status === "running");
assert.ok(running.length >= 1, "expected at least one running tool");
assert.ok(agents.length >= 1, "expected agent thought from fixture");
const line = formatToolLine(tools);
assert.match(line, /read_file|✓|◐/);
});
it("returns empty quietly for blank input", () => {
const { tools, agents, todos } = parseUpdatesLines(["", "not-json", "{}"]);
assert.deepEqual(tools, []);
assert.deepEqual(agents, []);
assert.deepEqual(todos, []);
});
});
+41
View File
@@ -0,0 +1,41 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
clampPercent,
contextPercentFromSignals,
formatTokenCount,
renderBar,
formatDuration,
projectLabel,
} from "../src/bar.js";
describe("bar", () => {
it("derives context percent from contextWindowUsage", () => {
assert.equal(
contextPercentFromSignals({
contextWindowUsage: 37,
contextTokensUsed: 190000,
contextWindowTokens: 500000,
}),
37,
);
});
it("falls back to used/window ratio", () => {
assert.equal(
contextPercentFromSignals({
contextTokensUsed: 250000,
contextWindowTokens: 500000,
}),
50,
);
});
it("clamps and formats", () => {
assert.equal(clampPercent(150), 100);
assert.equal(renderBar(37, 10).length, 10);
assert.match(formatTokenCount(190000), /190k|190\.0k/);
assert.equal(formatDuration(4620), "1h 17m");
assert.equal(projectLabel("/Users/dex/demo/CoachFlow", 2), "demo/CoachFlow");
});
});
+86
View File
@@ -0,0 +1,86 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
normalizeBillingPayload,
getCreditUsage,
clearUsageCache,
} from "../src/billing.js";
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
describe("billing", () => {
it("normalizes present monthly used/limit fixture ({val})", () => {
const body = JSON.parse(
fs.readFileSync(path.join(root, "fixtures", "billing-present.json"), "utf8"),
);
const u = normalizeBillingPayload(body);
assert.equal(u.available, true);
assert.equal(u.used, 17510);
assert.equal(u.limit, 150000);
assert.ok(u.percent != null);
assert.ok(Math.abs((u.percent ?? 0) - (17510 / 150000) * 100) < 0.05);
assert.equal(u.period, "monthly");
});
it("normalizes weekly creditUsagePercent fixture", () => {
const body = JSON.parse(
fs.readFileSync(
path.join(root, "fixtures", "billing-weekly-percent.json"),
"utf8",
),
);
const u = normalizeBillingPayload(body);
assert.equal(u.available, true);
assert.equal(u.percent, 22);
assert.equal(u.period, "weekly");
assert.match(u.message ?? "", /GrokBuild/);
});
it("missing payload is unavailable without throw", () => {
const u = normalizeBillingPayload(null);
assert.equal(u.available, false);
assert.match(u.message ?? "", /unavailable/i);
});
it("getCreditUsage degrades when auth missing", async () => {
clearUsageCache();
const u = await getCreditUsage("/tmp/does-not-exist-grok-home-xyz", {
enabled: true,
cacheTtlMs: 1,
}, {
readAuth: () => null,
});
assert.equal(u.available, false);
assert.match(u.message ?? "", /unavailable|auth/i);
});
it("getCreditUsage uses injected fetch payload", async () => {
clearUsageCache();
const weekly = JSON.parse(
fs.readFileSync(
path.join(root, "fixtures", "billing-weekly-percent.json"),
"utf8",
),
);
const monthly = JSON.parse(
fs.readFileSync(path.join(root, "fixtures", "billing-present.json"), "utf8"),
);
let n = 0;
const u = await getCreditUsage("/tmp/fake-home-billing-inject", { cacheTtlMs: 1 }, {
readAuth: () => ({ token: "test-token" }),
fetchJson: async (url) => {
n += 1;
const body = url.includes("format=credits") ? weekly : monthly;
return { ok: true, status: 200, body };
},
});
assert.equal(u.available, true);
assert.equal(u.percent, 22);
assert.equal(u.used, 17510);
assert.equal(u.limit, 150000);
assert.ok(n >= 1);
});
});
+73
View File
@@ -0,0 +1,73 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs, runCli, helpText } from "../src/index.js";
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const fixtureSession = path.join(root, "fixtures", "session");
describe("cli", () => {
it("parseArgs handles watch and session-dir", () => {
const o = parseArgs([
"--watch",
"--session-dir",
"/tmp/x",
"--no-usage",
"--max-iterations",
"2",
]);
assert.equal(o.watch, true);
assert.equal(o.sessionDir, "/tmp/x");
assert.equal(o.noUsage, true);
assert.equal(o.maxIterations, 2);
});
it("runCli one-shot against fixture exits 0 with context", async () => {
const chunks: string[] = [];
const code = await runCli(
["--once", "--session-dir", fixtureSession, "--no-usage", "--no-color"],
{
stdout: (s) => chunks.push(s),
stderr: () => {},
},
);
assert.equal(code, 0);
const text = chunks.join("\n");
assert.match(text, /Context/);
assert.match(text, /37%/);
assert.match(text, /190k|500k/);
});
it("runCli watch with max-iterations produces frames", async () => {
const chunks: string[] = [];
const code = await runCli(
[
"--watch",
"--session-dir",
fixtureSession,
"--no-usage",
"--no-color",
"--max-iterations",
"2",
"--interval",
"10",
],
{
stdout: (s) => chunks.push(s),
stderr: () => {},
sleep: async () => {},
},
);
assert.equal(code, 0);
const text = chunks.join("\n");
assert.match(text, /37%/);
// at least two frames
const hits = text.match(/37%/g) ?? [];
assert.ok(hits.length >= 2, `expected ≥2 frames, got ${hits.length}`);
});
it("help is non-empty", () => {
assert.match(helpText(), /grok-build-hud/);
});
});
+84
View File
@@ -0,0 +1,84 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
titleLine,
refreshDashboard,
runDashboardLoop,
ttyForPid,
} from "../src/dashboard.js";
import { loadSnapshotFromDir } from "../src/session.js";
const pkgRoot = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"..",
"..",
);
const fixtureSession = path.join(pkgRoot, "fixtures", "session");
describe("dashboard", () => {
it("titleLine includes ctx percent", () => {
const snap = loadSnapshotFromDir(fixtureSession)!;
const t = titleLine(snap, {
available: true,
percent: 22,
period: "weekly",
used: 17510,
limit: 150000,
});
assert.match(t, /◆/);
assert.match(t, /37%/);
assert.match(t, /22%|quota/);
});
it("ttyForPid returns null for bogus pid", () => {
assert.equal(ttyForPid(99999999), null);
});
it("refreshDashboard writes status files for fixture home", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-dash-"));
const sid = "fixture-session-001";
const dir = path.join(tmp, "sessions", "proj", sid);
fs.mkdirSync(dir, { recursive: true });
for (const f of ["signals.json", "summary.json", "updates.jsonl"]) {
fs.copyFileSync(path.join(fixtureSession, f), path.join(dir, f));
}
fs.writeFileSync(
path.join(tmp, "active_sessions.json"),
JSON.stringify([
{ session_id: sid, pid: process.pid, cwd: "/Users/dex/demo/CoachFlow" },
]),
);
const r = await refreshDashboard({ grokHome: tmp, noUsage: true });
assert.ok(r.session);
assert.match(r.title, /37%/);
assert.ok(fs.existsSync(path.join(tmp, "hud", "status-line.txt")));
assert.ok(fs.existsSync(path.join(tmp, "hud", "tmux-status.txt")));
fs.rmSync(tmp, { recursive: true, force: true });
});
it("runDashboardLoop respects maxIterations", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-loop-"));
const sid = "fixture-session-001";
const dir = path.join(tmp, "sessions", "proj", sid);
fs.mkdirSync(dir, { recursive: true });
for (const f of ["signals.json", "summary.json", "updates.jsonl"]) {
fs.copyFileSync(path.join(fixtureSession, f), path.join(dir, f));
}
const code = await runDashboardLoop({
grokHome: tmp,
intervalMs: 1,
maxIterations: 2,
noUsage: true,
writePid: true,
sleep: async () => {},
});
assert.equal(code, 0);
fs.rmSync(tmp, { recursive: true, force: true });
});
});
+140
View File
@@ -0,0 +1,140 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
parseHookPayload,
resolveSessionForHook,
runHookTick,
shouldEmitAnnotation,
} from "../src/hook.js";
import { findSessionDirById } from "../src/session.js";
import { formatCompactLine, writeStatusFiles } from "../src/status.js";
import { loadSnapshotFromDir } from "../src/session.js";
import { installGlobalHooks, buildHookManifest } from "../src/install.js";
const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const fixtureSession = path.join(pkgRoot, "fixtures", "session");
describe("hook / live status", () => {
it("parses hook payload sessionId", () => {
const p = parseHookPayload(
JSON.stringify({ sessionId: "abc", cwd: "/tmp/x", hookEventName: "stop" }),
);
assert.equal(p.sessionId, "abc");
assert.equal(p.cwd, "/tmp/x");
});
it("formatCompactLine includes context percent from fixture", () => {
const snap = loadSnapshotFromDir(fixtureSession)!;
assert.ok(snap);
const line = formatCompactLine(snap, null);
assert.match(line, /\[hud\]/);
assert.match(line, /37%/);
assert.match(line, /Grok 4\.5|ctx|tools/);
});
it("writeStatusFiles creates status-line.txt", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-"));
const snap = loadSnapshotFromDir(fixtureSession)!;
const r = writeStatusFiles(snap, null, tmp);
assert.ok(fs.existsSync(r.compactPath));
const text = fs.readFileSync(r.compactPath, "utf8");
assert.match(text, /37%/);
fs.rmSync(tmp, { recursive: true, force: true });
});
it("runHookTick against fixture via session-dir home layout", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-home-"));
// Build mini grok home with fixture session
const sid = "fixture-session-001";
const dir = path.join(
tmp,
"sessions",
encodeURIComponent("/Users/dex/demo/CoachFlow"),
sid,
);
fs.mkdirSync(dir, { recursive: true });
for (const f of ["signals.json", "summary.json", "updates.jsonl"]) {
fs.copyFileSync(path.join(fixtureSession, f), path.join(dir, f));
}
fs.writeFileSync(
path.join(tmp, "active_sessions.json"),
JSON.stringify([
{
session_id: sid,
pid: process.pid,
cwd: "/Users/dex/demo/CoachFlow",
},
]),
);
const found = findSessionDirById(tmp, sid);
assert.ok(found);
const result = await runHookTick({
payloadRaw: JSON.stringify({ sessionId: sid, hookEventName: "stop" }),
env: {
GROK_SESSION_ID: sid,
GROK_HOOK_EVENT: "stop",
},
grokHome: tmp,
forceAnnotate: true,
noUsage: true,
});
assert.equal(result.code, 0);
assert.ok(result.compact);
assert.match(result.compact!, /37%/);
assert.ok(fs.existsSync(path.join(tmp, "hud", "status-line.txt")));
fs.rmSync(tmp, { recursive: true, force: true });
});
it("shouldEmitAnnotation always true for Stop", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-th-"));
assert.equal(shouldEmitAnnotation("stop", tmp), true);
assert.equal(shouldEmitAnnotation("Stop", tmp), true);
fs.rmSync(tmp, { recursive: true, force: true });
});
it("buildHookManifest points at node + hook.js", () => {
const m = buildHookManifest("/abs/hook.js") as {
hooks: { Stop: { hooks: { command: string }[] }[] };
};
assert.match(m.hooks.Stop[0]!.hooks[0]!.command, /hook\.js/);
});
it("installGlobalHooks writes ~/.grok style hooks file", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-inst-"));
// ensure dist hook exists (built)
const hookJs = path.join(pkgRoot, "dist", "src", "hook.js");
assert.ok(fs.existsSync(hookJs), "dist must be built before this test");
const { hooksPath } = installGlobalHooks({ grokHome: tmp, root: pkgRoot });
assert.ok(fs.existsSync(hooksPath));
const body = fs.readFileSync(hooksPath, "utf8");
assert.match(body, /Stop/);
assert.match(body, /hook\.js/);
fs.rmSync(tmp, { recursive: true, force: true });
});
it("resolveSessionForHook uses env session id", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-res-"));
const sid = "fixture-session-001";
const dir = path.join(tmp, "sessions", "proj", sid);
fs.mkdirSync(dir, { recursive: true });
for (const f of ["signals.json", "summary.json", "updates.jsonl"]) {
fs.copyFileSync(path.join(fixtureSession, f), path.join(dir, f));
}
const snap = resolveSessionForHook(
{},
{ GROK_SESSION_ID: sid },
tmp,
);
assert.ok(snap);
assert.equal(snap!.sessionId, sid);
assert.equal(snap!.contextPercent, 37);
fs.rmSync(tmp, { recursive: true, force: true });
});
});
+74
View File
@@ -0,0 +1,74 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadSnapshotFromDir } from "../src/session.js";
import { renderHud, renderTmux } from "../src/render.js";
import { contextPercentFromSignals } from "../src/bar.js";
import type { UsageSnapshot } from "../src/types.js";
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const fixtureSession = path.join(root, "fixtures", "session");
describe("session + render", () => {
it("loads fixture session with known context values", () => {
const snap = loadSnapshotFromDir(fixtureSession);
assert.ok(snap);
assert.equal(snap!.sessionId, "fixture-session-001");
assert.equal(snap!.model, "grok-4.5");
assert.equal(snap!.cwd, "/Users/dex/demo/CoachFlow");
assert.equal(snap!.contextPercent, 37);
assert.equal(snap!.contextTokensUsed, 190000);
assert.equal(snap!.contextWindowTokens, 500000);
// Must use same formula as production bar module
assert.equal(
snap!.contextPercent,
contextPercentFromSignals(snap!.signals),
);
assert.ok(snap!.tools.length > 0);
});
it("renders HUD with context bar matching fixture percent", () => {
const snap = loadSnapshotFromDir(fixtureSession)!;
const usage: UsageSnapshot = {
available: true,
percent: 22,
used: 17510,
limit: 150000,
period: "weekly",
message: "GrokBuild 9%",
};
const text = renderHud(snap, usage, { color: false });
assert.match(text, /Context/);
assert.match(text, /37%/);
assert.match(text, /190k\/500k|190\.0k\/500k/);
assert.match(text, /Usage|Quota|22%/);
assert.match(text, /22%/);
assert.match(text, /Grok|grok/i);
assert.match(text, /CoachFlow/);
});
it("renders when usage unavailable without crash", () => {
const snap = loadSnapshotFromDir(fixtureSession)!;
const text = renderHud(
snap,
{ available: false, message: "usage unavailable" },
{ color: false },
);
assert.match(text, /37%/);
assert.match(text, /unavailable/i);
});
it("tmux line includes context percent", () => {
const snap = loadSnapshotFromDir(fixtureSession)!;
const line = renderTmux(snap, null, {
color: false,
tmux: true,
compact: false,
pathLevels: 2,
warningThreshold: 70,
criticalThreshold: 90,
});
assert.match(line, /ctx 37%/);
});
});
+83
View File
@@ -0,0 +1,83 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadSnapshotFromDir } from "../src/session.js";
import {
formatStatusBlock,
formatTmuxStatusLines,
writeStatusFiles,
} from "../src/status.js";
import { PRESET_FULL } from "../src/hud-config.js";
import { THEME_TOKYONIGHT } from "../src/theme.js";
import fs from "node:fs";
import os from "node:os";
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const fixture = path.join(root, "fixtures", "session");
describe("Claude-HUD parity status", () => {
it("formatStatusBlock has model line + context + usage lines", () => {
const snap = loadSnapshotFromDir(fixture)!;
const text = formatStatusBlock(
snap,
{
available: true,
percent: 23,
period: "weekly",
used: 18000,
limit: 150000,
resetsIn: "4d",
message: "GrokBuild 10%",
},
PRESET_FULL,
);
assert.match(text, /\[Grok 4\.5\]/);
assert.match(text, /Context/);
assert.match(text, /37%/);
assert.match(text, /Usage/);
assert.match(text, /23%/);
// multi-line
assert.ok(text.split("\n").length >= 2);
});
it("formatTmuxStatusLines returns 3 rows in full preset", () => {
const snap = loadSnapshotFromDir(fixture)!;
// inject todos/agents for line 3
snap.todos = [
{ content: "Ship HUD parity", status: "in_progress" },
{ content: "Write tests", status: "completed" },
];
const lines = formatTmuxStatusLines(
snap,
{ available: true, percent: 22, period: "weekly" },
THEME_TOKYONIGHT,
PRESET_FULL,
);
assert.equal(lines.length, 3);
assert.match(lines[0]!, /Grok 4\.5|CoachFlow/);
assert.match(lines[1]!, /Context|Usage|22%|37%/);
});
it("writeStatusFiles emits tmux-lines.txt", () => {
const snap = loadSnapshotFromDir(fixture)!;
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-full-"));
// point grok home at tmp with config
fs.mkdirSync(path.join(tmp, "hud"), { recursive: true });
fs.writeFileSync(
path.join(tmp, "hud", "config.json"),
JSON.stringify(PRESET_FULL, null, 2),
);
// session needs to be written via writeStatusFiles with grokHome=tmp
// but session is loaded from fixture; OK
const r = writeStatusFiles(
snap,
{ available: true, percent: 20, period: "weekly" },
tmp,
);
assert.ok(fs.existsSync(path.join(tmp, "hud", "tmux-lines.txt")));
assert.ok(r.tmuxLines.length >= 2);
assert.match(r.full, /Context/);
fs.rmSync(tmp, { recursive: true, force: true });
});
});
+84
View File
@@ -0,0 +1,84 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
import { fileURLToPath } from "node:url";
import { formatTmuxStatusLine } from "../src/status.js";
import { loadSnapshotFromDir } from "../src/session.js";
import {
miniBar,
THEME_TOKYONIGHT,
THEME_GROKDAY,
paletteForGrokTheme,
readGrokUiConfig,
resolveTheme,
normalizeGrokThemeName,
} from "../src/theme.js";
const pkgRoot = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"..",
"..",
);
const fixtureSession = path.join(pkgRoot, "fixtures", "session");
describe("theme", () => {
it("maps Grok theme names to palettes", () => {
assert.equal(normalizeGrokThemeName("TokyoNight"), "tokyonight");
assert.equal(paletteForGrokTheme("tokyonight").name, "tokyonight");
assert.equal(paletteForGrokTheme("grokday").name, "grokday");
assert.equal(paletteForGrokTheme("light").name, "grokday");
assert.equal(paletteForGrokTheme("dark").name, "groknight");
});
it("tokyonight palette uses Tokyo Night ink", () => {
assert.equal(THEME_TOKYONIGHT.value, "#c0caf5");
assert.equal(THEME_TOKYONIGHT.ok, "#9ece6a");
assert.equal(THEME_TOKYONIGHT.mark, "#7aa2f7");
});
it("readGrokUiConfig parses [ui] theme", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-cfg-"));
fs.writeFileSync(
path.join(tmp, "config.toml"),
`[ui]\ntheme = "tokyonight"\nauto_dark_theme = "tokyonight"\nauto_light_theme = "grokday"\n`,
);
const cfg = readGrokUiConfig(tmp);
assert.equal(cfg.theme, "tokyonight");
assert.equal(cfg.autoLightTheme, "grokday");
fs.rmSync(tmp, { recursive: true, force: true });
});
it("resolveTheme follows Grok config when mode is auto", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-cfg2-"));
fs.writeFileSync(
path.join(tmp, "config.toml"),
`[ui]\ntheme = "tokyonight"\n`,
);
const t = resolveTheme("auto", { GROK_HUD_THEME: "auto" }, { grokHome: tmp });
assert.equal(t.name, "tokyonight");
fs.rmSync(tmp, { recursive: true, force: true });
});
it("miniBar works with tokyonight", () => {
const bar = miniBar(50, 8, THEME_TOKYONIGHT);
assert.match(bar, /#9ece6a|#e0af68|#f7768e/);
});
it("formatTmuxStatusLine uses provided palette colours", () => {
const snap = loadSnapshotFromDir(fixtureSession)!;
const line = formatTmuxStatusLine(
snap,
{ available: true, percent: 22, message: "GrokBuild 9%" },
THEME_TOKYONIGHT,
);
assert.match(line, /#c0caf5|#565f89|#7aa2f7|#9ece6a/);
assert.match(line, /37%/);
});
it("grokday is light-paper friendly", () => {
assert.equal(THEME_GROKDAY.value, "#111111");
assert.match(THEME_GROKDAY.barEmpty, /#dcdcdc/);
});
});