BranchDashboard: Electron

Dashboard: Electron

A desktop app with a window. Between web and native.

Get Your Agent to Help

Install the Electron skill (5.7K installs — the most popular):

npx skills add vercel-labs/agent-browser@electron

Then ask: "help me build an Electron app that shows my launchd job status using IPC to a CLI"

Create the App

mkdir -p ~/Code/jobs-electron && cd ~/Code/jobs-electron
npm init -y
npm install electron --save-dev

Architecture

Electron splits into main process (Node.js, can run CLI) and renderer (browser, shows UI). They talk via IPC:

main.js  →  execSync("bun run cli.ts status")  →  JSON
   ↕ IPC
renderer (index.html)  →  window.jobs.status()  →  render

Main Process

main.js — IPC handlers call the CLI:

const { app, BrowserWindow, ipcMain } = require("electron");
const { execSync } = require("child_process");
const path = require("path");

const BUN = path.join(process.env.HOME, ".bun/bin/bun");
const CLI = "/path/to/james-long-ai-job-scheduling/src/cli.ts"; // ← update

function run(cmd) {
  try {
    return JSON.parse(execSync(`${BUN} run ${CLI} ${cmd}`, { encoding: "utf-8", timeout: 10_000 }));
  } catch (err) {
    return { ok: false, error: { message: err.message } };
  }
}

ipcMain.handle("jobs:status", () => run("status"));
ipcMain.handle("jobs:sync", () => run("sync"));
ipcMain.handle("jobs:kick", (_, name) => run(`kick ${name}`));
ipcMain.handle("jobs:logs", (_, name) => run(`logs ${name}`));

app.whenReady().then(() => {
  const win = new BrowserWindow({
    width: 800, height: 600,
    webPreferences: { preload: path.join(__dirname, "preload.js") },
  });
  win.loadFile("index.html");
});

app.on("window-all-closed", () => app.quit());

Preload

preload.js — exposes CLI calls to the renderer:

const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("jobs", {
  status: () => ipcRenderer.invoke("jobs:status"),
  sync: () => ipcRenderer.invoke("jobs:sync"),
  kick: (name) => ipcRenderer.invoke("jobs:kick", name),
  logs: (name) => ipcRenderer.invoke("jobs:logs", name),
});

Renderer

index.html — same as the web branch, but use window.jobs.status() instead of fetch:

async function load() {
  const data = await window.jobs.status();
  // data.result.jobs — render them
}

Ask your coding agent to build the full HTML. It knows Electron.

Run It

npx electron .

Why Electron

Real desktop window. ⌘+Tab to it. System tray icon possible. Notifications via Electron API. It's more than a browser tab but less work than Swift.

Companion Notes

Branch: Electron Dashboard

A desktop app with a real window. If you want something between a web page and a native app.

Setup

mkdir -p ~/Code/ai-jobs-dashboard
cd ~/Code/ai-jobs-dashboard
npm init -y
npm install electron --save-dev

Architecture

Electron main process → spawns CLI commands
        ↓ IPC
Electron renderer → displays job state (same HTML as web branch)

The key insight: the renderer is basically the same HTML from the web branch, but instead of fetch('/api/status') it uses Electron's IPC to call the CLI.

Main Process

Create main.js:

const { app, BrowserWindow, ipcMain } = require("electron");
const { execSync } = require("child_process");
const path = require("path");

// Adjust these paths
const BUN = path.join(process.env.HOME, ".bun/bin/bun");
const CLI = "/path/to/james-long-ai-job-scheduling/src/cli.ts";

function runCLI(command) {
  try {
    const stdout = execSync(`${BUN} run ${CLI} ${command}`, {
      encoding: "utf-8",
      timeout: 10_000,
    });
    return JSON.parse(stdout);
  } catch (err) {
    return { ok: false, error: { message: err.message } };
  }
}

// IPC handlers — renderer calls these
ipcMain.handle("jobs:status", () => runCLI("status"));
ipcMain.handle("jobs:sync", () => runCLI("sync"));
ipcMain.handle("jobs:kick", (_, name) => runCLI(`kick ${name}`));
ipcMain.handle("jobs:logs", (_, name) => runCLI(`logs ${name}`));
ipcMain.handle("jobs:doctor", (_, name) => runCLI(name ? `doctor ${name}` : "doctor"));

let mainWindow;

app.whenReady().then(() => {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    titleBarStyle: "hiddenInset",
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  });

  mainWindow.loadFile("index.html");
});

app.on("window-all-closed", () => app.quit());

Preload

Create preload.js:

const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("jobs", {
  status: () => ipcRenderer.invoke("jobs:status"),
  sync: () => ipcRenderer.invoke("jobs:sync"),
  kick: (name) => ipcRenderer.invoke("jobs:kick", name),
  logs: (name) => ipcRenderer.invoke("jobs:logs", name),
  doctor: (name) => ipcRenderer.invoke("jobs:doctor", name),
});

Renderer

Create index.html — same structure as the web branch, but replace fetch('/api/status') with:

// Instead of:   const resp = await fetch('/api/status');
// Use:          const data = await window.jobs.status();

async function load() {
  const data = await window.jobs.status();
  // ... render exactly like the web branch
}

The actions become:

// Kick a job
await window.jobs.kick(jobName);

// View logs
const logData = await window.jobs.logs(jobName);

// Sync
await window.jobs.sync();

Run It

npx electron .

Add to package.json: "start": "electron ."

Why Electron Over Web

  • Real desktop window — ⌘+Tab to it, minimize, full screen
  • System notifications via Electron's Notification API
  • Tray icon (like the SwiftUI menubar but cross-platform)
  • File system access without a server (IPC handles it)

What to Add

  • Tray icon — show job count, click to open
  • Notifications — alert on job failure
  • Log viewer — split pane with auto-scrolling log tail
  • Dark/light theme — match macOS appearance