"use client"; import { Suspense, useEffect, useState } from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { ArrowLeft, CheckCircle2, ExternalLink, Github, KeyRound, Play, Plus, Trash2, Webhook, XCircle, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { api, type Agent, type AuthStatus, type FlowSummary, type Run, type ServerConfig, } from "@/lib/api"; import { formatDate } from "@/lib/utils"; export default function AgentDetailPage() { return ( Loading…}> ); } function AgentDetail() { const router = useRouter(); const params = useSearchParams(); const id = params.get("id") ?? ""; const [agent, setAgent] = useState(null); const [config, setConfig] = useState(null); const [pipelines, setPipelines] = useState([]); const [runs, setRuns] = useState([]); const [error, setError] = useState(null); const [busy, setBusy] = useState(null); // which action is in flight const [showPipelinePicker, setShowPipelinePicker] = useState(false); async function load() { if (!id) return; try { const [a, cfg, allPipes] = await Promise.all([ api.getAgent(id), api.getConfig().catch(() => null), api.listFlows().catch(() => []), ]); setAgent(a); setConfig(cfg); setPipelines(allPipes); // Pull recent runs across all attached pipelines. if (a.attachedPipelines?.length) { const lists = await Promise.all( a.attachedPipelines.map((pid) => api.listRuns({ pipelineId: pid, limit: 5 }).catch(() => []) ) ); const merged = lists.flat(); merged.sort((x, y) => (y.startedAt || "").localeCompare(x.startedAt || "")); setRuns(merged.slice(0, 10)); } else { setRuns([]); } setError(null); } catch (e) { setError(e instanceof Error ? e.message : "load failed"); } } useEffect(() => { load(); // Reload recent runs whenever any run is dispatched (manual / agent / // GitHub push). Cheap — `load()` is one round-trip. const es = new EventSource(api.runsStreamURL()); es.onmessage = () => load(); es.onerror = () => {}; return () => es.close(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]); async function withBusy(label: string, fn: () => Promise) { setBusy(label); setError(null); try { return await fn(); } catch (e) { setError(e instanceof Error ? e.message : `${label} failed`); } finally { setBusy(null); } } async function onTrigger() { await withBusy("trigger", async () => { const res = await api.triggerAgent(id); const ids = res.executionIds ?? []; if (ids.length === 1) { router.push(`/executions/view/?id=${encodeURIComponent(ids[0])}`); return; } if (ids.length > 1) { router.push(`/executions/multi/?ids=${ids.map(encodeURIComponent).join(",")}`); return; } // Nothing dispatched — surface failures so the user sees why. const fails = res.failures ?? []; if (fails.length === 0) { throw new Error("trigger returned no executions and no failure detail"); } throw new Error( fails .map((f) => `${f.pipelineId}: ${f.reason}${f.error ? " — " + f.error : ""}`) .join("; ") ); }); } async function onTestAuth() { await withBusy("test-auth", async () => { await api.testAgentAuth(id); await load(); }); } async function onInstallWebhook() { await withBusy("webhook-install", async () => { await api.installAgentWebhook(id); await load(); }); } async function onUninstallWebhook() { if (!confirm("Uninstall the GitHub webhook for this agent?")) return; await withBusy("webhook-uninstall", async () => { await api.uninstallAgentWebhook(id); await load(); }); } async function onAttachPipeline(pipelineId: string) { await withBusy("attach", async () => { await api.attachPipeline(id, pipelineId); setShowPipelinePicker(false); await load(); }); } async function onDetachPipeline(pipelineId: string) { if (!confirm("Detach this pipeline from the agent?")) return; await withBusy("detach", async () => { await api.detachPipeline(id, pipelineId); await load(); }); } async function onDeleteAgent() { if (!confirm("Delete this agent? Webhook will be removed too.")) return; await withBusy("delete", async () => { await api.deleteAgent(id); router.push("/agents"); }); } if (!id) { return (
Missing id query param.
); } if (!agent) { return (
{error ? (

{error}

) : (

Loading agent…

)}
); } const lastRun = runs[0]; const attachedPipelineDetails = (agent.attachedPipelines ?? []) .map((pid) => pipelines.find((p) => p.id === pid)) .filter((p): p is FlowSummary => Boolean(p)); const attachable = pipelines.filter( (p) => !agent.attachedPipelines?.includes(p.id) ); return (

{agent.name}

{agent.id}

{error && ( {error} )} {/* Overview ---------------------------------------------------------- */} Overview
{agent.name}
{lastRun ? (
{formatDate(lastRun.startedAt)}
) : ( No runs yet. )}
{attachedPipelineDetails.length === 0 ? ( None attached.{" "} Create one . ) : ( {attachedPipelineDetails.length} attached )}
{agent.attachedPipelines?.length ? (

Pushes to{" "} {agent.name} route through this agent’s pipelines (matched by branch).

) : null} {agent.webhookUrl && (

{agent.webhookUrl}

)}
{/* Repo / webhook -------------------------------------------------- */} Repo
{agent.name}
Token{" "} {agent.hasPat ? ( ******** ) : ( not set )}
Auth: {agent.authCheckedAt && ( · {formatDate(agent.authCheckedAt)} )}
Webhook: {agent.webhookInstalledAt && ( · {formatDate(agent.webhookInstalledAt)} )}
{agent.webhookUrl && (

{agent.webhookUrl}

)} {!config?.webhooksAvailable && !agent.webhookInstalled && (

FLOW_PUBLIC_URL is not configured on the server. Set it (e.g. to a cloudflared tunnel) to install webhooks.

)}
{agent.webhookInstalled ? ( ) : ( )}
{/* Pipelines ----------------------------------------------------- */} Pipelines {attachable.length > 0 ? ( ) : ( )} {attachedPipelineDetails.length === 0 ? (

No pipelines attached. Click “Add pipeline” to bind one (or create one in{" "} /flows/new ).

) : (
    {attachedPipelineDetails.map((p) => (
  • {p.name || "Untitled"}
    {p.nodeCount} nodes · {formatDate(p.updatedAt)}
  • ))}
)} {showPipelinePicker && attachable.length > 0 && (
Attach a pipeline
    {attachable.map((p) => (
  • ))}
)}
{/* Recent runs ------------------------------------------------------ */} Recent runs {runs.length > 0 && ( View all → )} {runs.length === 0 ? (

No runs yet — trigger one from the Overview block above.

) : (
    {runs.map((r) => (
  • {r.id}
    {r.pipelineName || r.pipelineId} ·{" "} {formatDate(r.startedAt)}
  • ))}
)}
); } function Field({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } function AuthBadge({ status }: { status: AuthStatus }) { if (status === "ok") { return ( auth ok ); } if (status === "failed") { return ( auth failed ); } return auth untested; } function WebhookBadge({ agent }: { agent: Agent }) { if (agent.webhookInstalled) { return ( webhook installed ); } return ( no webhook ); } function RunStatus({ status }: { status: string }) { const s = status.toLowerCase(); if (s === "success" || s === "completed") { return ( {status} ); } if (s === "failed" || s === "error") { return ( {status} ); } return {status}; } // Avoid unused-import lint when the symbol is referenced only by type. void KeyRound; void ExternalLink;