BranchDashboard: Raycast Extension

Dashboard: Raycast Extension

Job status from ⌘+Space. The slickest option if you use Raycast.

Get Your Agent to Help

Install the Raycast extension skill:

npx skills add johnlindquist/claude@raycast-extension

Then ask: "help me build a Raycast extension that shows my launchd job status"

Create the Extension

In Raycast, run "Create Extension" → pick "List" template. Or from the terminal:

mkdir -p ~/Code/jobs-raycast && cd ~/Code/jobs-raycast
npm init raycast-extension -- --name "AI Jobs" --type list
npm install

The Command

Replace src/index.tsx. The key: useExec calls bun run cli.ts status and parses the JSON:

import { List, ActionPanel, Action, showToast, Toast, Icon, Color } from "@raycast/api";
import { useExec } from "@raycast/utils";

// IMPORTANT: absolute paths — Raycast has minimal PATH
const BUN = `${process.env.HOME}/.bun/bin/bun`;
const CLI = "/path/to/james-long-ai-job-scheduling/src/cli.ts"; // ← update this

export default function Command() {
  const { data, isLoading, revalidate } = useExec(BUN, ["run", CLI, "status"], {
    parseOutput: ({ stdout }) => JSON.parse(stdout).result.jobs,
  });

  return (
    <List isLoading={isLoading} searchBarPlaceholder="Filter jobs...">
      {data?.map((job: any) => (
        <List.Item
          key={job.label}
          title={job.name}
          subtitle={job.schedule}
          icon={{
            source: job.loaded ? Icon.CheckCircle : Icon.Circle,
            tintColor: !job.loaded ? Color.Yellow
              : job.exitCode !== "0" && job.exitCode !== "-" ? Color.Red
              : Color.Green,
          }}
          accessories={[{ text: job.scope }, { text: job.loaded ? "loaded" : "unloaded" }]}
          actions={
            <ActionPanel>
              <Action title="Kick (Run Now)" icon={Icon.Play}
                onAction={async () => {
                  Bun.spawnSync([BUN, "run", CLI, "kick", job.name]);
                  showToast({ style: Toast.Style.Success, title: `Kicked ${job.name}` });
                  revalidate();
                }}
              />
              <Action title="Sync All" icon={Icon.ArrowClockwise}
                onAction={async () => {
                  Bun.spawnSync([BUN, "run", CLI, "sync"]);
                  showToast({ style: Toast.Style.Success, title: "Synced" });
                  revalidate();
                }}
              />
            </ActionPanel>
          }
        />
      ))}
    </List>
  );
}

Develop

npm run dev
# Extension appears in Raycast with hot reload

Why Raycast

  • ⌘+Space → "AI Jobs" → see everything instantly
  • ActionPanel gives each job contextual actions (kick, sync, view logs)
  • useExec + JSON CLI = no server needed
  • It's a native macOS experience, not a browser tab

Companion Notes

Branch: Raycast Extension

A Raycast command that shows job status, logs, and lets you kick/sync jobs from ⌘+Space. The slickest option if you already use Raycast.

Setup

# Install Raycast CLI tools
npm install -g @raycast/api

# Create the extension
cd ~/Code
npx create-raycast-extension --name ai-job-scheduler --type list
cd ai-job-scheduler

The Main Command

Replace src/index.tsx:

import { List, ActionPanel, Action, showToast, Toast, Icon, Color } from "@raycast/api";
import { useExec } from "@raycast/utils";
import { useState } from "react";

// Path to the CLI — adjust to your project
const CLI = "/path/to/james-long-ai-job-scheduling/src/cli.ts";
const BUN = "/Users/YOU/.bun/bin/bun"; // absolute path — Raycast has minimal PATH

interface Job {
  name: string;
  label: string;
  scope: string;
  schedule: string;
  loaded: boolean;
  pid: string;
  exitCode: string;
  disabled: boolean;
}

function statusIcon(job: Job) {
  if (job.disabled) return { source: Icon.Circle, tintColor: Color.SecondaryText };
  if (!job.loaded) return { source: Icon.Circle, tintColor: Color.Yellow };
  if (job.exitCode !== "0" && job.exitCode !== "-") return { source: Icon.ExclamationMark, tintColor: Color.Red };
  if (job.pid !== "-") return { source: Icon.CircleProgress, tintColor: Color.Green };
  return { source: Icon.CheckCircle, tintColor: Color.Green };
}

function statusText(job: Job): string {
  if (job.disabled) return "Disabled";
  if (!job.loaded) return "Unloaded";
  if (job.exitCode !== "0" && job.exitCode !== "-") return `Error (exit ${job.exitCode})`;
  if (job.pid !== "-") return `Running (PID ${job.pid})`;
  return "Idle";
}

export default function Command() {
  const { data, isLoading, revalidate } = useExec(BUN, ["run", CLI, "status"], {
    parseOutput: ({ stdout }) => {
      const parsed = JSON.parse(stdout);
      return parsed.result.jobs as Job[];
    },
  });

  return (
    <List isLoading={isLoading} searchBarPlaceholder="Filter jobs...">
      {data?.map((job) => (
        <List.Item
          key={job.label}
          title={job.name}
          subtitle={job.schedule}
          icon={statusIcon(job)}
          accessories={[
            { text: job.scope },
            { text: statusText(job) },
          ]}
          actions={
            <ActionPanel>
              <Action
                title="View Logs"
                icon={Icon.Terminal}
                onAction={async () => {
                  // Open logs in a detail view — see LogDetail below
                }}
              />
              <Action
                title="Kick (Run Now)"
                icon={Icon.Play}
                shortcut={{ modifiers: ["cmd"], key: "k" }}
                onAction={async () => {
                  const proc = Bun.spawnSync([BUN, "run", CLI, "kick", job.name]);
                  const result = JSON.parse(new TextDecoder().decode(proc.stdout));
                  if (result.ok) {
                    showToast({ style: Toast.Style.Success, title: `Kicked ${job.name}` });
                  } else {
                    showToast({ style: Toast.Style.Failure, title: result.error.message });
                  }
                  revalidate();
                }}
              />
              <Action
                title="Sync All Jobs"
                icon={Icon.ArrowClockwise}
                shortcut={{ modifiers: ["cmd"], key: "s" }}
                onAction={async () => {
                  Bun.spawnSync([BUN, "run", CLI, "sync"]);
                  showToast({ style: Toast.Style.Success, title: "Synced" });
                  revalidate();
                }}
              />
              <Action
                title="View Plist"
                icon={Icon.Document}
                onAction={async () => {
                  // Show plist XML in a detail view
                }}
              />
              <Action
                title="Run Doctor"
                icon={Icon.Stethoscope}
                shortcut={{ modifiers: ["cmd"], key: "d" }}
                onAction={async () => {
                  const proc = Bun.spawnSync([BUN, "run", CLI, "doctor", job.name]);
                  const result = JSON.parse(new TextDecoder().decode(proc.stdout));
                  const issues = result.result.checks[0]?.issues || [];
                  if (issues.length === 0) {
                    showToast({ style: Toast.Style.Success, title: "Healthy" });
                  } else {
                    showToast({
                      style: Toast.Style.Failure,
                      title: `${issues.length} issues`,
                      message: issues[0].message,
                    });
                  }
                }}
              />
            </ActionPanel>
          }
        />
      ))}
    </List>
  );
}

Key Raycast Patterns

  • useExec calls the CLI and parses JSON — the agent-first output pays off
  • revalidate refreshes after mutations (kick, sync)
  • ActionPanel gives each job contextual actions
  • Absolute paths — Raycast has minimal PATH, use full paths to bun and the CLI

Development

cd ai-job-scheduler
npm run dev
# Opens in Raycast dev mode — hot reload

What to Add

  • Log detail viewAction.Push to a Detail component showing jobs logs <name> output
  • Plist detail — show the generated XML in a Detail with markdown code block
  • Doctor summary — a separate command showing overall health
  • Menu bar command — persistent icon showing job count + health