[taskboard] add dispatch control plane
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user