BranchDashboard: SwiftUI Menubar

Dashboard: SwiftUI Menubar

A native macOS menubar app. The most ambitious branch.

Get Your Agent to Help

Install the macOS design skill:

npx skills add ehmo/platform-design-skills@macos-design-guidelines

Then ask: "help me build a SwiftUI MenuBarExtra app that shows my launchd job status by calling a CLI"

Create the Project

mkdir -p ~/Code/jobs-menubar && cd ~/Code/jobs-menubar

Create Package.swift:

// swift-tools-version: 5.9
import PackageDescription
let package = Package(
    name: "AIJobsMenubar",
    platforms: [.macOS(.v14)],
    targets: [.executableTarget(name: "AIJobsMenubar", path: "Sources")]
)

The App

Sources/AIJobsMenubarApp.swiftMenuBarExtra with .window style gives you a popover:

import SwiftUI

@main
struct AIJobsMenubarApp: App {
    var body: some Scene {
        MenuBarExtra("AI Jobs", systemImage: "gearshape.2") {
            JobsView()
        }
        .menuBarExtraStyle(.window)
    }
}

The View

Sources/JobsView.swift — shell out to the CLI, parse JSON, render:

import SwiftUI

struct Job: Codable, Identifiable {
    let name, label, scope, schedule, pid, exitCode: String
    let loaded, disabled: Bool
    var id: String { label }
    var statusColor: Color {
        if !loaded { return .yellow }
        if exitCode != "0" && exitCode != "-" { return .red }
        return .green
    }
}

struct CLIResponse: Codable {
    let ok: Bool
    let result: StatusResult
}
struct StatusResult: Codable {
    let total, loaded: Int
    let jobs: [Job]
}

@Observable class JobsModel {
    var jobs: [Job] = []
    // IMPORTANT: absolute path to bun
    let bun = "\(NSHomeDirectory())/.bun/bin/bun"
    let cli: String

    init() {
        // Update this path to your project
        cli = "\(NSHomeDirectory())/Code/badass-courses/james-long-ai-job-scheduling/src/cli.ts"
    }

    func refresh() {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: bun)
        process.arguments = ["run", cli, "status"]
        let pipe = Pipe()
        process.standardOutput = pipe
        try? process.run()
        process.waitUntilExit()
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        if let response = try? JSONDecoder().decode(CLIResponse.self, from: data) {
            jobs = response.result.jobs
        }
    }

    func kick(_ job: Job) {
        let p = Process()
        p.executableURL = URL(fileURLWithPath: bun)
        p.arguments = ["run", cli, "kick", job.name]
        try? p.run(); p.waitUntilExit()
        refresh()
    }
}

struct JobsView: View {
    @State private var model = JobsModel()
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            ForEach(model.jobs) { job in
                HStack {
                    Circle().fill(job.statusColor).frame(width: 8, height: 8)
                    Text(job.name).font(.system(.body, design: .monospaced))
                    Spacer()
                    Text(job.schedule).font(.caption).foregroundStyle(.secondary)
                }
                .contentShape(Rectangle())
                .onTapGesture { model.kick(job) }
            }
            Divider()
            HStack {
                Button("Refresh") { model.refresh() }
                Spacer()
            }.buttonStyle(.plain).font(.caption)
        }
        .padding()
        .frame(width: 350)
        .onAppear { model.refresh() }
    }
}

Build & Run

swift build && swift run
# Menubar icon appears

Why This Branch

You're building a real Mac app. It lives in the menubar. It's native. Click a job to kick it. This is the most ambitious branch — your coding agent can scaffold it, but you're writing Swift.

Companion Notes

Branch: SwiftUI Menubar App

A native macOS menubar app that shows job status at a glance. Click the icon → see all jobs. The most ambitious branch — you're building a real Mac app.

Architecture

Menubar icon (SwiftUI) → shells out to `jobs status` (JSON)
                        → parses response
                        → renders in a popover

Setup

mkdir -p ~/Code/ai-jobs-menubar
cd ~/Code/ai-jobs-menubar

Create Package.swift:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "AIJobsMenubar",
    platforms: [.macOS(.v14)],
    targets: [
        .executableTarget(name: "AIJobsMenubar", path: "Sources")
    ]
)

The App

Create Sources/AIJobsMenubarApp.swift:

import SwiftUI

@main
struct AIJobsMenubarApp: App {
    var body: some Scene {
        MenuBarExtra("AI Jobs", systemImage: "gearshape.2") {
            JobsMenuView()
        }
        .menuBarExtraStyle(.window)
    }
}

The View

Create Sources/JobsMenuView.swift:

import SwiftUI

struct Job: Codable, Identifiable {
    let name: String
    let label: String
    let scope: String
    let schedule: String
    let loaded: Bool
    let pid: String
    let exitCode: String
    let disabled: Bool

    var id: String { label }

    var statusColor: Color {
        if disabled { return .gray }
        if !loaded { return .yellow }
        if exitCode != "0" && exitCode != "-" { return .red }
        return .green
    }

    var statusText: String {
        if disabled { return "Disabled" }
        if !loaded { return "Unloaded" }
        if exitCode != "0" && exitCode != "-" { return "Error (\(exitCode))" }
        if pid != "-" { return "Running" }
        return "Idle"
    }
}

struct CLIResponse: Codable {
    let ok: Bool
    let result: StatusResult
}

struct StatusResult: Codable {
    let total: Int
    let loaded: Int
    let jobs: [Job]
}

@Observable
class JobsModel {
    var jobs: [Job] = []
    var lastUpdate: Date?
    var error: String?

    // IMPORTANT: absolute path to bun — SwiftUI apps have minimal PATH
    let bunPath = "\(NSHomeDirectory())/.bun/bin/bun"
    let cliPath: String

    init() {
        // Adjust this path to your project
        cliPath = "\(NSHomeDirectory())/Code/badass-courses/james-long-ai-job-scheduling/src/cli.ts"
    }

    func refresh() {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: bunPath)
        process.arguments = ["run", cliPath, "status"]

        let pipe = Pipe()
        process.standardOutput = pipe

        do {
            try process.run()
            process.waitUntilExit()

            let data = pipe.fileHandleForReading.readDataToEndOfFile()
            let response = try JSONDecoder().decode(CLIResponse.self, from: data)
            jobs = response.result.jobs
            lastUpdate = Date()
            error = nil
        } catch {
            self.error = error.localizedDescription
        }
    }

    func kick(_ job: Job) {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: bunPath)
        process.arguments = ["run", cliPath, "kick", job.name]
        try? process.run()
        process.waitUntilExit()
        refresh()
    }

    func sync() {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: bunPath)
        process.arguments = ["run", cliPath, "sync"]
        try? process.run()
        process.waitUntilExit()
        refresh()
    }
}

struct JobsMenuView: View {
    @State private var model = JobsModel()

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            ForEach(model.jobs) { job in
                HStack {
                    Circle()
                        .fill(job.statusColor)
                        .frame(width: 8, height: 8)
                    Text(job.name)
                        .font(.system(.body, design: .monospaced))
                    Spacer()
                    Text(job.schedule)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                    Text(job.statusText)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
                .contentShape(Rectangle())
                .onTapGesture { model.kick(job) }
            }

            Divider()

            HStack {
                Button("Sync") { model.sync() }
                Button("Refresh") { model.refresh() }
                Spacer()
                if let date = model.lastUpdate {
                    Text(date, style: .time)
                        .font(.caption2)
                        .foregroundStyle(.tertiary)
                }
            }
            .buttonStyle(.plain)
            .font(.caption)

            if let error = model.error {
                Text(error)
                    .font(.caption)
                    .foregroundStyle(.red)
            }
        }
        .padding()
        .frame(width: 400)
        .onAppear { model.refresh() }
    }
}

Build & Run

cd ~/Code/ai-jobs-menubar
swift build
swift run
# → Menubar icon appears

To run at login, add the built binary to System Settings → Login Items.

What Makes This Special

  • Native macOS — feels like a real system tool
  • Menubar = always visible, never in the way
  • Click a job to kick it
  • The JSON CLI is the entire backend — SwiftUI is just rendering

What to Add

  • Auto-refresh timer (poll every 30s)
  • Color the menubar icon based on health (green/yellow/red)
  • Notification on job failure (use UNUserNotificationCenter)
  • Log viewer in a separate window