bb6a1321fb
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.
181 lines
5.1 KiB
TypeScript
181 lines
5.1 KiB
TypeScript
import {
|
|
formatDuration,
|
|
formatTokenCount,
|
|
projectLabel,
|
|
renderBar,
|
|
} from "./bar.js";
|
|
import { formatToolLine } from "./activity.js";
|
|
import type {
|
|
RenderOptions,
|
|
SessionSnapshot,
|
|
UsageSnapshot,
|
|
} from "./types.js";
|
|
|
|
const ANSI = {
|
|
reset: "\x1b[0m",
|
|
dim: "\x1b[2m",
|
|
bold: "\x1b[1m",
|
|
green: "\x1b[32m",
|
|
yellow: "\x1b[33m",
|
|
red: "\x1b[31m",
|
|
cyan: "\x1b[36m",
|
|
magenta: "\x1b[35m",
|
|
blue: "\x1b[34m",
|
|
};
|
|
|
|
function colorize(
|
|
text: string,
|
|
color: string | null,
|
|
enabled: boolean,
|
|
): string {
|
|
if (!enabled || !color) return text;
|
|
return `${color}${text}${ANSI.reset}`;
|
|
}
|
|
|
|
function severityColor(
|
|
percent: number,
|
|
opts: RenderOptions,
|
|
): string {
|
|
if (percent >= opts.criticalThreshold) return ANSI.red;
|
|
if (percent >= opts.warningThreshold) return ANSI.yellow;
|
|
return ANSI.green;
|
|
}
|
|
|
|
export function renderHud(
|
|
session: SessionSnapshot,
|
|
usage: UsageSnapshot | null,
|
|
opts: Partial<RenderOptions> = {},
|
|
): string {
|
|
const options: RenderOptions = {
|
|
color: opts.color ?? true,
|
|
tmux: opts.tmux ?? false,
|
|
compact: opts.compact ?? false,
|
|
pathLevels: opts.pathLevels ?? 2,
|
|
warningThreshold: opts.warningThreshold ?? 70,
|
|
criticalThreshold: opts.criticalThreshold ?? 90,
|
|
};
|
|
|
|
if (options.tmux) {
|
|
return renderTmux(session, usage, options);
|
|
}
|
|
|
|
const c = options.color;
|
|
const model = colorize(displayModel(session.model), ANSI.cyan, c);
|
|
const project = projectLabel(session.cwd, options.pathLevels);
|
|
const branchPart = session.branch
|
|
? ` git:(${session.branch}${session.gitDirty ? "*" : ""})`
|
|
: "";
|
|
const live = session.live
|
|
? colorize("● live", ANSI.green, c)
|
|
: colorize("○ stale", ANSI.dim, c);
|
|
|
|
const line1 = `[${model}] │ ${project}${branchPart} │ ${live}`;
|
|
|
|
const pct = Math.round(session.contextPercent);
|
|
const barColor = severityColor(session.contextPercent, options);
|
|
const bar = colorize(renderBar(session.contextPercent), barColor, c);
|
|
const tokenPart =
|
|
session.contextWindowTokens > 0
|
|
? ` (${formatTokenCount(session.contextTokensUsed)}/${formatTokenCount(session.contextWindowTokens)})`
|
|
: "";
|
|
const ctxLabel = colorize("Context", ANSI.bold, c);
|
|
const meta: string[] = [];
|
|
if (session.durationSeconds > 0) {
|
|
meta.push(`Time ${formatDuration(session.durationSeconds)}`);
|
|
}
|
|
if (session.turnCount > 0) meta.push(`Turns ${session.turnCount}`);
|
|
if (session.toolCallCount > 0) meta.push(`Tools ${session.toolCallCount}`);
|
|
const line2 = `${ctxLabel} ${bar} ${pct}%${tokenPart}${meta.length ? ` │ ${meta.join(" │ ")}` : ""}`;
|
|
|
|
const lines = [line1, line2];
|
|
|
|
const usageLine = formatUsageLine(usage, options);
|
|
if (usageLine) lines.push(usageLine);
|
|
|
|
const toolLine = formatToolLine(session.tools);
|
|
if (toolLine) {
|
|
lines.push(toolLine);
|
|
}
|
|
|
|
if (session.agents?.length) {
|
|
const a = session.agents[session.agents.length - 1]!;
|
|
lines.push(
|
|
`◎ agent ${a.title ?? "agent"}${a.detail ? `: ${a.detail}` : ""}`,
|
|
);
|
|
}
|
|
|
|
if (options.compact) {
|
|
return lines.slice(0, 2).join(" · ");
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function displayModel(model: string): string {
|
|
if (!model || model === "unknown") return "Grok";
|
|
// grok-4.5 -> Grok 4.5
|
|
return model
|
|
.replace(/^grok-/i, "Grok ")
|
|
.replace(/-/g, " ");
|
|
}
|
|
|
|
function formatUsageLine(
|
|
usage: UsageSnapshot | null | undefined,
|
|
opts: RenderOptions,
|
|
): string {
|
|
if (!usage) {
|
|
return `Quota ${colorize("—", ANSI.dim, opts.color)} unavailable`;
|
|
}
|
|
if (!usage.available) {
|
|
return `Quota ${colorize("—", ANSI.dim, opts.color)} ${usage.message ?? "unavailable"}`;
|
|
}
|
|
const pct = usage.percent ?? 0;
|
|
const bar = colorize(renderBar(pct), severityColor(pct, opts), opts.color);
|
|
const nums =
|
|
usage.used != null && usage.limit != null
|
|
? ` · month ${formatTokenCount(usage.used)}/${formatTokenCount(usage.limit)}`
|
|
: "";
|
|
const period = usage.period ? ` (${usage.period})` : "";
|
|
const reset = usage.resetsIn ? ` · resets ${usage.resetsIn}` : "";
|
|
const prod = usage.message ? ` · ${usage.message}` : "";
|
|
return `Quota ${bar} ${Math.round(pct)}%${period}${nums}${reset}${prod}`;
|
|
}
|
|
|
|
export function renderTmux(
|
|
session: SessionSnapshot,
|
|
usage: UsageSnapshot | null,
|
|
opts: RenderOptions,
|
|
): string {
|
|
const pct = Math.round(session.contextPercent);
|
|
const project = projectLabel(session.cwd, opts.pathLevels);
|
|
const u =
|
|
usage?.available && usage.percent != null
|
|
? ` u${Math.round(usage.percent)}%`
|
|
: "";
|
|
return `ctx ${pct}%${u} ${displayModel(session.model)} ${project}`;
|
|
}
|
|
|
|
export function renderJson(
|
|
session: SessionSnapshot,
|
|
usage: UsageSnapshot | null,
|
|
): string {
|
|
return JSON.stringify(
|
|
{
|
|
sessionId: session.sessionId,
|
|
model: session.model,
|
|
cwd: session.cwd,
|
|
live: session.live,
|
|
contextPercent: session.contextPercent,
|
|
contextTokensUsed: session.contextTokensUsed,
|
|
contextWindowTokens: session.contextWindowTokens,
|
|
turnCount: session.turnCount,
|
|
toolCallCount: session.toolCallCount,
|
|
tools: session.tools,
|
|
agents: session.agents,
|
|
usage,
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
}
|