From 2ec17712c9f132315aaaeb2730bebf38045867a5 Mon Sep 17 00:00:00 2001 From: Christopher Mayor Date: Sat, 7 Mar 2026 13:57:39 -0800 Subject: [PATCH] [taskboard] add agent detail pages --- app/agents/[slug]/page.tsx | 271 +++++++++++++++++++++++++++++++++ app/api/agents/[slug]/route.ts | 16 ++ components/agents-client.tsx | 27 ++++ lib/agents.ts | 5 + 4 files changed, 319 insertions(+) create mode 100644 app/agents/[slug]/page.tsx create mode 100644 app/api/agents/[slug]/route.ts diff --git a/app/agents/[slug]/page.tsx b/app/agents/[slug]/page.tsx new file mode 100644 index 0000000..2396416 --- /dev/null +++ b/app/agents/[slug]/page.tsx @@ -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 ( +
+
+
+ + Back to agents + +

+ {agent.emoji} + {agent.name} +

+

{agent.role}

+
+
+ {agent.family} + {agent.status} + {agent.host} +
+
+ +
+ + + Overview + Runtime, routing, heartbeat, and capability details for this agent. + + +
+
+
Host
+
{agent.host}
+
+
+
Model
+
{agent.model || "Host-local/runtime-defined"}
+
+
+
Dispatch
+
{agent.defaultDispatchMethod}
+
+
+
Heartbeat
+
{agent.heartbeatAt ? formatDateTime(agent.heartbeatAt) : "No heartbeat"}
+
+
+
Runtime Path
+
{agent.runtimePath}
+
+ {agent.configPath ? ( +
+
Config Path
+
{agent.configPath}
+
+ ) : null} +
+
Current Task
+
{agent.currentTask || "No heartbeat task"}
+
+
+
Failure Count
+
{agent.failureStreak}
+
+
+ +
+
+

Channels

+
+ {agent.channels.map((channel) => ( + + {channel.label}: {channel.value} + + ))} +
+
+ +
+

Tools

+
+ {agent.tools.length ? ( + agent.tools.map((tool) => ( + + {tool} + + )) + ) : ( +

No parsed tools.

+ )} +
+
+ +
+

Capabilities

+
+ {agent.capabilities.length ? ( + agent.capabilities.map((capability) => ( + + {capability} + + )) + ) : ( +

No parsed capabilities.

+ )} +
+
+
+
+
+ + + + Activity Snapshot + Recent event and assignment health for this agent. + + +
+
+

Active

+

{agent.activeTasks.length}

+
+
+

Completed

+

{agent.completedTasks.length}

+
+
+ +
+

Last Event

+ {agent.lastEvent ? ( +
+
+ + {agent.lastEvent.event_type} + + + {agent.lastEvent.state || "n/a"} + +
+

{agent.lastEvent.summary}

+

+ {agent.lastEvent.detail || "No detail captured."} +

+

+ {formatDateTime(agent.lastEvent.created_at)} +

+
+ ) : ( +

No audit events recorded yet.

+ )} +
+ + {agent.notes.length ? ( +
+

Notes

+
+ {agent.notes.map((note) => ( +
+ {note} +
+ ))} +
+
+ ) : null} +
+
+
+ +
+ + + Assigned Tasks + Current active work assigned to this agent. + + + {agent.activeTasks.length ? ( + agent.activeTasks.map((task) => ( +
+
+
+

{task.title}

+

{task.description}

+
+ {task.dispatch_state} +
+
+ {task.status} + {task.priority} + {task.tags.slice(0, 4).map((tag) => ( + + {tag} + + ))} +
+
+ )) + ) : ( +

No active tasks assigned.

+ )} +
+
+ + + + Recently Completed + Latest finished work attributed to this agent. + + + {agent.completedTasks.length ? ( + agent.completedTasks.map((task) => ( +
+
+
+

{task.title}

+ {task.result_summary ? ( +

{task.result_summary}

+ ) : ( +

{task.description}

+ )} +
+ done +
+

+ {formatDateTime(task.completed_at || task.updated_at)} +

+
+ )) + ) : ( +

No recently completed tasks.

+ )} +
+
+
+
+ ); +} diff --git a/app/api/agents/[slug]/route.ts b/app/api/agents/[slug]/route.ts new file mode 100644 index 0000000..493b8b0 --- /dev/null +++ b/app/api/agents/[slug]/route.ts @@ -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); +} diff --git a/components/agents-client.tsx b/components/agents-client.tsx index 8fe6f71..3b29821 100644 --- a/components/agents-client.tsx +++ b/components/agents-client.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { useDeferredValue, useState } from "react"; import { Badge } from "@/components/ui/badge"; @@ -148,6 +149,25 @@ export function AgentsClient({ )} +
+

Assigned Tasks

+ {agent.activeTasks.length ? ( +
+ {agent.activeTasks.slice(0, 3).map((task) => ( +
+
+

{task.title}

+ {task.status} +
+

{task.result_summary || task.description}

+
+ ))} +
+ ) : ( +

No assigned tasks.

+ )} +
+

Tools

@@ -162,6 +182,13 @@ export function AgentsClient({ )}
+ + + View Agent Details + ))} diff --git a/lib/agents.ts b/lib/agents.ts index 6c983de..34eb1f7 100644 --- a/lib/agents.ts +++ b/lib/agents.ts @@ -344,6 +344,11 @@ export async function findAgentByAssignmentKey(assignmentKey: string) { 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() { const agents = await listFleetAgents(); return {