BranchDashboard: Terminal (TUI)

Dashboard: Terminal (TUI)

A terminal dashboard consuming the NDJSON stream. No GUI.

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 watch as 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