Get Your Agent to Help
Install a TUI skill:
npx skills add msmps/opentui-skill@opentui
Then ask: "help me build a terminal dashboard that consumes jobs watch NDJSON and renders a live job table"
The One-Liner
This works right now, no code needed:
watch -n 5 'bun run src/cli.ts status | python3 -c "
import json,sys
d=json.load(sys.stdin)
for j in d[\"result\"][\"jobs\"]:
s=\"✓\" if j[\"loaded\"] else \"✗\"
print(f\" {s} {j[\"name\"]:24s} {j[\"schedule\"]:20s} exit={j[\"exitCode\"]}\")"'
That's a dashboard. Refreshes every 5 seconds.
The Real Version
A bun script that subscribes to jobs watch and renders a live table:
#!/usr/bin/env bun
import { spawn } from "node:child_process";
import { join } from "node:path";
const CLI = join(import.meta.dir, "cli.ts");
const c = (code: number, s: string) => `\x1b[${code}m${s}\x1b[0m`;
function render(snapshot: any) {
process.stdout.write("\x1b[2J\x1b[H"); // clear screen
console.log(c(90, `AI Jobs — ${new Date().toLocaleTimeString()}`));
console.log(c(90, "─".repeat(70)));
console.log(` ${c(1, "NAME".padEnd(24))} ${c(1, "SCHEDULE".padEnd(20))} ${c(1, "STATUS".padEnd(10))} ${c(1, "EXIT")}`);
for (const job of snapshot.jobs) {
const status = !job.loaded ? c(33, "unloaded") :
job.exitCode !== "0" && job.exitCode !== "-" ? c(31, "error") :
job.pid !== "-" ? c(32, "running") : c(32, "idle");
console.log(` ${job.name.padEnd(24)} ${c(90, job.schedule.padEnd(20))} ${status.padEnd(19)} ${job.exitCode}`);
}
console.log(c(90, `\n${snapshot.total} jobs · ${snapshot.loaded} loaded · Ctrl+C to exit`));
}
const child = spawn("bun", ["run", CLI, "watch", "--interval", "5"], { stdio: ["ignore", "pipe", "inherit"] });
let buf = "";
child.stdout.on("data", (chunk: Buffer) => {
buf += chunk.toString();
const lines = buf.split("\n");
buf = lines.pop() || "";
for (const line of lines) {
if (!line.trim()) continue;
try {
const event = JSON.parse(line);
if (event.type === "snapshot") render(event);
} catch {}
}
});
process.on("SIGINT", () => { child.kill(); process.exit(0); });
Run It
bun run src/dashboard.ts
Why TUI
No browser. No Electron. No Swift. Just your terminal. The jobs watch NDJSON stream is the API — the TUI is a thin rendering layer. Add keyboard navigation (j/k to select, Enter to kick) when you want more.
Companion Notes
Branch: Terminal Dashboard (TUI)
A full-screen terminal dashboard that refreshes live. No GUI, no browser, no dependencies beyond the CLI.
The Simplest Version
One-liner that works right now:
watch -n 5 'bun run src/cli.ts status | python3 -c "
import json,sys
d=json.load(sys.stdin)
for j in d[\"result\"][\"jobs\"]:
s=\"✓\" if j[\"loaded\"] else \"✗\"
print(f\" {s} {j[\"name\"]:24s} {j[\"schedule\"]:20s} exit={j[\"exitCode\"]}\")"'
That's a dashboard. It refreshes every 5 seconds. Done.
The Better Version
A bun script that uses jobs watch and renders a live table:
Create src/dashboard.ts:
#!/usr/bin/env bun
import { spawn } from "node:child_process";
import { resolve, join } from "node:path";
const CLI = join(resolve(import.meta.dir), "cli.ts");
function clearScreen() {
process.stdout.write("\x1b[2J\x1b[H");
}
function color(code: number, text: string): string {
return `\x1b[${code}m${text}\x1b[0m`;
}
function renderSnapshot(snapshot: any) {
clearScreen();
const now = new Date().toLocaleTimeString();
console.log(color(90, `AI Job Scheduler — ${now}`));
console.log(color(90, `${"─".repeat(70)}`));
console.log(
` ${color(1, "NAME".padEnd(24))} ${color(1, "SCHEDULE".padEnd(20))} ${color(1, "STATUS".padEnd(10))} ${color(1, "EXIT")}`,
);
console.log(color(90, ` ${"─".repeat(24)} ${"─".repeat(20)} ${"─".repeat(10)} ${"─".repeat(4)}`));
for (const job of snapshot.jobs) {
const status = !job.loaded
? color(33, "unloaded".padEnd(10))
: job.exitCode !== "0" && job.exitCode !== "-"
? color(31, "error".padEnd(10))
: job.pid !== "-"
? color(32, "running".padEnd(10))
: color(32, "idle".padEnd(10));
console.log(
` ${job.name.padEnd(24)} ${color(90, job.schedule.padEnd(20))} ${status} ${job.exitCode.padEnd(4)}`,
);
}
console.log("");
console.log(color(90, `${snapshot.total} jobs · ${snapshot.loaded} loaded · ${snapshot.errored} errored`));
console.log(color(90, "Ctrl+C to exit"));
}
// Subscribe to the watch stream
const child = spawn("bun", ["run", CLI, "watch", "--interval", "5"], {
stdio: ["ignore", "pipe", "inherit"],
});
let buffer = "";
child.stdout.on("data", (chunk: Buffer) => {
buffer += chunk.toString();
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.trim()) continue;
try {
const event = JSON.parse(line);
if (event.type === "snapshot") {
renderSnapshot(event);
}
} catch {}
}
});
child.on("exit", () => process.exit(0));
process.on("SIGINT", () => {
child.kill();
process.exit(0);
});
Run It
bun run src/dashboard.ts
Add to package.json: "dashboard": "bun run src/dashboard.ts"
What Makes This Interesting
- Uses
jobs watchas the data source — same streaming backend any dashboard uses - ANSI colors for status (green/yellow/red)
- No dependencies — just process stdout rendering
- Demonstrates the pattern:
jobs watch→ parse NDJSON → render
What to Add
- Keyboard navigation (j/k to select, Enter to kick, l for logs)
- Split pane: jobs list on top, log tail on bottom
- Sparkline for job run frequency using braille characters
- Filter by scope or status