[taskboard] add agent detail pages
This commit is contained in:
271
app/agents/[slug]/page.tsx
Normal file
271
app/agents/[slug]/page.tsx
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { findAgentBySlug } from "@/lib/agents";
|
||||||
|
import { formatDateTime } from "@/lib/utils";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
function familyVariant(family: string) {
|
||||||
|
if (family === "zeroclaw") {
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
if (family === "direct") {
|
||||||
|
return "warning";
|
||||||
|
}
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchVariant(state: string) {
|
||||||
|
return state === "failed" ? "warning" : state === "completed" ? "success" : "secondary";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AgentDetailPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
}) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const agent = await findAgentBySlug(slug);
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<Link className="text-sm text-cyan-300/80 hover:text-cyan-200" href="/agents">
|
||||||
|
Back to agents
|
||||||
|
</Link>
|
||||||
|
<h1 className="mt-2 flex items-center gap-3 text-3xl font-semibold text-white">
|
||||||
|
<span>{agent.emoji}</span>
|
||||||
|
<span>{agent.name}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 max-w-3xl text-sm text-slate-300">{agent.role}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant={familyVariant(agent.family)}>{agent.family}</Badge>
|
||||||
|
<Badge variant="secondary">{agent.status}</Badge>
|
||||||
|
<Badge variant="outline">{agent.host}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_380px]">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Overview</CardTitle>
|
||||||
|
<CardDescription>Runtime, routing, heartbeat, and capability details for this agent.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-5 md:grid-cols-2">
|
||||||
|
<dl className="grid gap-3 text-sm text-slate-300">
|
||||||
|
<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">Heartbeat</dt>
|
||||||
|
<dd>{agent.heartbeatAt ? formatDateTime(agent.heartbeatAt) : "No heartbeat"}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Runtime Path</dt>
|
||||||
|
<dd className="break-all font-mono text-xs text-cyan-100">{agent.runtimePath}</dd>
|
||||||
|
</div>
|
||||||
|
{agent.configPath ? (
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Config Path</dt>
|
||||||
|
<dd className="break-all font-mono text-xs text-cyan-100">{agent.configPath}</dd>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<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">Failure Count</dt>
|
||||||
|
<dd>{agent.failureStreak}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div className="space-y-5">
|
||||||
|
<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">Tools</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{agent.tools.length ? (
|
||||||
|
agent.tools.map((tool) => (
|
||||||
|
<Badge key={tool} variant="secondary">
|
||||||
|
{tool}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-slate-400">No parsed tools.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Capabilities</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{agent.capabilities.length ? (
|
||||||
|
agent.capabilities.map((capability) => (
|
||||||
|
<Badge key={capability} variant="outline">
|
||||||
|
{capability}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-slate-400">No parsed capabilities.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Activity Snapshot</CardTitle>
|
||||||
|
<CardDescription>Recent event and assignment health for this agent.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
|
||||||
|
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">Active</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold text-white">{agent.activeTasks.length}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
|
||||||
|
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">Completed</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold text-white">{agent.completedTasks.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Last Event</p>
|
||||||
|
{agent.lastEvent ? (
|
||||||
|
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant={agent.lastEvent.event_type === "dispatch_failed" ? "warning" : "secondary"}>
|
||||||
|
{agent.lastEvent.event_type}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={dispatchVariant(agent.lastEvent.state || "planned")}>
|
||||||
|
{agent.lastEvent.state || "n/a"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 font-medium text-white">{agent.lastEvent.summary}</p>
|
||||||
|
<p className="mt-2 break-words text-sm text-slate-300">
|
||||||
|
{agent.lastEvent.detail || "No detail captured."}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-xs uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
{formatDateTime(agent.lastEvent.created_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-slate-400">No audit events recorded yet.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{agent.notes.length ? (
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Notes</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{agent.notes.map((note) => (
|
||||||
|
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-3 text-sm text-slate-300" key={note}>
|
||||||
|
{note}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 xl:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Assigned Tasks</CardTitle>
|
||||||
|
<CardDescription>Current active work assigned to this agent.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{agent.activeTasks.length ? (
|
||||||
|
agent.activeTasks.map((task) => (
|
||||||
|
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4" key={task.id}>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-medium text-white">{task.title}</p>
|
||||||
|
<p className="mt-1 break-words text-sm text-slate-300">{task.description}</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant={dispatchVariant(task.dispatch_state)}>{task.dispatch_state}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
<Badge variant="secondary">{task.status}</Badge>
|
||||||
|
<Badge variant="outline">{task.priority}</Badge>
|
||||||
|
{task.tags.slice(0, 4).map((tag) => (
|
||||||
|
<Badge key={tag} variant="outline">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-slate-400">No active tasks assigned.</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Recently Completed</CardTitle>
|
||||||
|
<CardDescription>Latest finished work attributed to this agent.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{agent.completedTasks.length ? (
|
||||||
|
agent.completedTasks.map((task) => (
|
||||||
|
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4" key={task.id}>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-medium text-white">{task.title}</p>
|
||||||
|
{task.result_summary ? (
|
||||||
|
<p className="mt-1 break-words text-sm text-slate-300">{task.result_summary}</p>
|
||||||
|
) : (
|
||||||
|
<p className="mt-1 break-words text-sm text-slate-300">{task.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Badge variant="success">done</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-xs uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
{formatDateTime(task.completed_at || task.updated_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-slate-400">No recently completed tasks.</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
app/api/agents/[slug]/route.ts
Normal file
16
app/api/agents/[slug]/route.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { findAgentBySlug } from "@/lib/agents";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_request: Request,
|
||||||
|
{ params }: { params: Promise<{ slug: string }> },
|
||||||
|
) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const agent = await findAgentBySlug(slug);
|
||||||
|
if (!agent) {
|
||||||
|
return NextResponse.json({ error: "agent_not_found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(agent);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
import { useDeferredValue, useState } from "react";
|
import { useDeferredValue, useState } from "react";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -148,6 +149,25 @@ export function AgentsClient({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Tools</p>
|
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Tools</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -162,6 +182,13 @@ export function AgentsClient({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -344,6 +344,11 @@ export async function findAgentByAssignmentKey(assignmentKey: string) {
|
|||||||
return agents.find((agent) => agent.assignmentKey === assignmentKey || agent.aliases.includes(assignmentKey)) || null;
|
return agents.find((agent) => agent.assignmentKey === assignmentKey || agent.aliases.includes(assignmentKey)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function findAgentBySlug(slug: string) {
|
||||||
|
const agents = await listFleetAgents();
|
||||||
|
return agents.find((agent) => agent.slug === slug) || null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function listArchitecture() {
|
export async function listArchitecture() {
|
||||||
const agents = await listFleetAgents();
|
const agents = await listFleetAgents();
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user