Files
openclaw-taskboard/components/agents-client.tsx

199 lines
8.3 KiB
TypeScript

"use client";
import Link from "next/link";
import { useDeferredValue, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import type { AgentFamily, FleetAgent } from "@/lib/types";
function heartbeatTone(agent: FleetAgent) {
if (agent.heartbeatAgeMinutes === null) {
return "warning";
}
if (agent.heartbeatAgeMinutes > 180) {
return "warning";
}
return "success";
}
export function AgentsClient({
agents,
defaultFamily = "",
}: {
agents: FleetAgent[];
defaultFamily?: AgentFamily | "";
}) {
const [query, setQuery] = useState("");
const [family, setFamily] = useState<AgentFamily | "">(defaultFamily);
const deferredQuery = useDeferredValue(query);
const filteredAgents = agents.filter((agent) => {
const matchesQuery =
deferredQuery.length === 0 ||
agent.name.toLowerCase().includes(deferredQuery.toLowerCase()) ||
agent.host.toLowerCase().includes(deferredQuery.toLowerCase()) ||
agent.role.toLowerCase().includes(deferredQuery.toLowerCase());
const matchesFamily = family.length === 0 || agent.family === family;
return matchesQuery && matchesFamily;
});
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Configured Agent Runtimes</CardTitle>
<CardDescription>
OpenClaw swarm members, ZeroClaw runtimes, and direct host targets from the tracked fleet model with heartbeat and dispatch overlays.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-3 md:grid-cols-[1fr_220px]">
<Input
placeholder="Search by name, host, or role"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
<Select value={family} onChange={(event) => setFamily(event.target.value as AgentFamily | "")}>
<option value="">All families</option>
<option value="openclaw">OpenClaw</option>
<option value="zeroclaw">ZeroClaw</option>
<option value="direct">Direct</option>
</Select>
</CardContent>
</Card>
<div className="grid gap-4 lg:grid-cols-2">
{filteredAgents.map((agent) => (
<Card key={agent.slug}>
<CardHeader>
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle className="flex items-center gap-2">
<span>{agent.emoji}</span>
<span>{agent.name}</span>
</CardTitle>
<CardDescription>{agent.role}</CardDescription>
</div>
<div className="flex gap-2">
<Badge variant={agent.family === "openclaw" ? "default" : agent.family === "zeroclaw" ? "success" : "warning"}>
{agent.family}
</Badge>
<Badge variant="secondary">{agent.status}</Badge>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<dl className="grid gap-2 text-sm text-slate-300 md:grid-cols-2">
<div>
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Host</dt>
<dd>{agent.host}</dd>
</div>
<div>
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Model</dt>
<dd>{agent.model || "Host-local/runtime-defined"}</dd>
</div>
<div>
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Dispatch</dt>
<dd>{agent.defaultDispatchMethod}</dd>
</div>
<div>
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Workload</dt>
<dd>{agent.workload} active</dd>
</div>
<div className="md:col-span-2">
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Runtime</dt>
<dd className="font-mono text-xs text-cyan-100">{agent.runtimePath}</dd>
</div>
<div>
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Current task</dt>
<dd>{agent.currentTask || "No heartbeat task"}</dd>
</div>
<div>
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Heartbeat</dt>
<dd>
<Badge variant={heartbeatTone(agent)}>
{agent.heartbeatAgeMinutes === null ? "No heartbeat" : `${agent.heartbeatAgeMinutes}m ago`}
</Badge>
</dd>
</div>
</dl>
<div>
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Channels</p>
<div className="flex flex-wrap gap-2">
{agent.channels.map((channel) => (
<Badge key={`${agent.slug}-${channel.label}`} variant="outline">
{channel.label}: {channel.value}
</Badge>
))}
</div>
</div>
<div>
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Last dispatch event</p>
{agent.lastEvent ? (
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-3">
<p className="font-medium text-white">{agent.lastEvent.summary}</p>
<p className="mt-1 text-sm text-slate-300">{agent.lastEvent.detail || "No detail captured."}</p>
<div className="mt-2 flex flex-wrap gap-2">
<Badge variant={agent.lastEvent.event_type === "dispatch_failed" ? "warning" : "secondary"}>
{agent.lastEvent.event_type}
</Badge>
<Badge variant="outline">Failures: {agent.failureStreak}</Badge>
</div>
</div>
) : (
<p className="text-sm text-slate-400">No audit events recorded yet.</p>
)}
</div>
<div>
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Assigned Tasks</p>
{agent.activeTasks.length ? (
<div className="space-y-2">
{agent.activeTasks.slice(0, 3).map((task) => (
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-3" key={task.id}>
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="font-medium text-white">{task.title}</p>
<Badge variant="secondary">{task.status}</Badge>
</div>
<p className="mt-1 text-sm text-slate-300">{task.result_summary || task.description}</p>
</div>
))}
</div>
) : (
<p className="text-sm text-slate-400">No assigned tasks.</p>
)}
</div>
<div>
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Tools</p>
<div className="flex flex-wrap gap-2">
{agent.tools.length ? (
agent.tools.map((tool) => (
<Badge key={tool} variant="secondary">
{tool}
</Badge>
))
) : (
<span className="text-sm text-slate-400">No parsed tools.</span>
)}
</div>
</div>
<Link
className="inline-flex items-center justify-center rounded-lg border border-cyan-300/20 bg-cyan-300/10 px-4 py-2 text-sm font-medium text-cyan-100 transition hover:border-cyan-300/40 hover:bg-cyan-300/15"
href={`/agents/${agent.slug}`}
>
View Agent Details
</Link>
</CardContent>
</Card>
))}
</div>
</div>
);
}