This commit is contained in:
Dr Alex Mitre 2026-01-30 04:45:56 -07:00 committed by GitHub
commit 990579ea76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 141 additions and 0 deletions

View File

@ -2,6 +2,7 @@ import type { Command } from "commander";
import { healthCommand } from "../../commands/health.js"; import { healthCommand } from "../../commands/health.js";
import { sessionsCommand } from "../../commands/sessions.js"; import { sessionsCommand } from "../../commands/sessions.js";
import { statusCommand } from "../../commands/status.js"; import { statusCommand } from "../../commands/status.js";
import { monitorCommand } from "../../commands/monitor.command.js";
import { setVerbose } from "../../globals.js"; import { setVerbose } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js"; import { formatDocsLink } from "../../terminal/links.js";
@ -77,6 +78,28 @@ export function registerStatusHealthSessionsCommands(program: Command) {
}); });
}); });
program
.command("monitor")
.description("Live TUI dashboard for monitoring Moltbot status")
.option("--interval <ms>", "Refresh interval in milliseconds", "2000")
.addHelpText(
"after",
() =>
`\n${theme.heading("Examples:")}\n${formatHelpExamples([
["moltbot monitor", "Start the live monitor."],
["moltbot monitor --interval 5000", "Update every 5 seconds."],
])}\n`,
)
.action(async (opts) => {
const interval = parseTimeoutMs(opts.interval);
if (interval === null) {
return;
}
await runCommandWithRuntime(defaultRuntime, async () => {
await monitorCommand({ intervalMs: interval }, defaultRuntime);
});
});
program program
.command("health") .command("health")
.description("Fetch health from the running gateway") .description("Fetch health from the running gateway")

View File

@ -0,0 +1,9 @@
import type { RuntimeEnv } from "../runtime.js";
import { runMonitorTui } from "../tui/monitor.js";
export async function monitorCommand(
opts: { intervalMs?: number },
runtime: RuntimeEnv,
) {
await runMonitorTui(runtime, { intervalMs: opts.intervalMs ?? 2000 });
}

109
src/tui/monitor.ts Normal file
View File

@ -0,0 +1,109 @@
import { Container, Text, TUI, ProcessTerminal } from "@mariozechner/pi-tui";
import { scanStatus } from "../commands/status.scan.js";
import { theme } from "./theme/theme.js";
import type { RuntimeEnv } from "../runtime.js";
import { formatDuration } from "../commands/status.format.js";
import { resolveUpdateAvailability } from "../commands/status.update.js";
export async function runMonitorTui(runtime: RuntimeEnv, opts: { intervalMs: number }) {
const tui = new TUI(new ProcessTerminal());
const root = new Container();
const header = new Text("", 1, 0);
const statusLine = new Text("", 1, 0);
const resourcesLine = new Text("", 1, 0);
const agentsHeader = new Text(theme.header("Agents & Sessions"), 1, 0);
const agentsText = new Text("", 10, 0); // Fixed height for now
const footer = new Text("", 1, 0);
root.addChild(header);
root.addChild(statusLine);
root.addChild(resourcesLine);
root.addChild(new Text(" ", 1, 0)); // Spacer
root.addChild(agentsHeader);
root.addChild(agentsText);
root.addChild(new Text(" ", 1, 0)); // Spacer
root.addChild(footer);
tui.addChild(root);
let running = true;
let lastUpdate = 0;
const refresh = async () => {
if (!running) return;
try {
const now = Date.now();
const status = await scanStatus({ json: true, timeoutMs: opts.intervalMs - 200 }, runtime);
const latency = status.gatewayProbe?.connectLatencyMs
? formatDuration(status.gatewayProbe.connectLatencyMs)
: "N/A";
const gatewayState = status.gatewayReachable
? theme.success("Online")
: theme.error("Offline");
const updateLatency = Date.now() - now;
lastUpdate = now;
const updates = resolveUpdateAvailability(status.update);
const updateMsg = updates.available ? `${theme.accent("UPDATE AVAILABLE")}` : "";
header.setText(
theme.header(`Moltbot Monitor • ${new Date().toLocaleTimeString()}${updateMsg}`),
);
statusLine.setText(
`Gateway: ${gatewayState} • Latency: ${latency} • Ver: ${status.gatewaySelf?.version ?? "unknown"} • Mode: ${status.gatewayMode}`,
);
const mem = status.memory;
const memStatus = mem ? `${mem.files} files, ${mem.chunks} chunks` : "Disabled/Unknown";
resourcesLine.setText(`Memory: ${memStatus} • OS: ${status.osSummary.label}`);
// Agents & Sessions
const agents = status.agentStatus.agents;
const sessions = status.summary.sessions.recent;
let agentLines: string[] = [];
if (agents.length === 0) {
agentLines.push(theme.dim("No agents found."));
} else {
agentLines.push(`${agents.length} Agent(s) Active`);
}
if (sessions.length > 0) {
agentLines.push("");
agentLines.push("Recent Sessions:");
for (const s of sessions.slice(0, 5)) {
agentLines.push(`${s.key} (${s.model ?? "default"}) - ${formatDuration(s.age)} ago`);
}
} else {
agentLines.push("");
agentLines.push(theme.dim("No active sessions."));
}
agentsText.setText(agentLines.join("\n"));
footer.setText(theme.dim(`Update took ${updateLatency}ms. Press Ctrl+C to exit.`));
tui.requestRender();
} catch (err) {
footer.setText(theme.error(`Error: ${err}`));
}
if (running) {
setTimeout(refresh, opts.intervalMs);
}
};
tui.start();
refresh();
process.on("SIGINT", () => {
running = false;
tui.stop();
process.exit(0);
});
}