[taskboard] add dispatch control plane

This commit is contained in:
2026-03-06 15:21:19 -08:00
parent 1699f0f2b7
commit be1cf8ca8d
25 changed files with 1594 additions and 292 deletions

View File

@@ -1,28 +1,43 @@
"use client";
import { useMemo, useState } from "react";
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 { FleetAgent } from "@/lib/types";
import type { AgentFamily, FleetAgent } from "@/lib/types";
export function AgentsClient({ agents }: { agents: FleetAgent[] }) {
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("");
const [family, setFamily] = useState<AgentFamily | "">(defaultFamily);
const deferredQuery = useDeferredValue(query);
const filteredAgents = useMemo(() => {
return agents.filter((agent) => {
const matchesQuery =
query.length === 0 ||
agent.name.toLowerCase().includes(query.toLowerCase()) ||
agent.host.toLowerCase().includes(query.toLowerCase()) ||
agent.role.toLowerCase().includes(query.toLowerCase());
const matchesFamily = family.length === 0 || agent.family === family;
return matchesQuery && matchesFamily;
});
}, [agents, family, 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">
@@ -30,12 +45,16 @@ export function AgentsClient({ agents }: { agents: FleetAgent[] }) {
<CardHeader>
<CardTitle>Configured Agent Runtimes</CardTitle>
<CardDescription>
OpenClaw swarm members and ZeroClaw host runtimes are shown from the deployed fleet model.
OpenClaw swarm members and ZeroClaw host runtimes 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)}>
<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>
@@ -71,18 +90,30 @@ export function AgentsClient({ agents }: { agents: FleetAgent[] }) {
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Model</dt>
<dd>{agent.model || "Host-local/runtime-defined"}</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>
<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>
@@ -96,10 +127,36 @@ export function AgentsClient({ agents }: { agents: FleetAgent[] }) {
</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">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>}
{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>
</CardContent>