diff --git a/ui/src/pages/Agents.tsx b/ui/src/pages/Agents.tsx index 1388a7da..32aa5889 100644 --- a/ui/src/pages/Agents.tsx +++ b/ui/src/pages/Agents.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { agentsApi, type OrgNode } from "../api/agents"; @@ -30,12 +30,27 @@ const roleLabels: Record = { type FilterTab = "all" | "active" | "paused" | "error"; +function matchesFilter(status: string, tab: FilterTab): boolean { + if (tab === "all") return true; + if (tab === "active") return status === "active" || status === "running" || status === "idle"; + if (tab === "paused") return status === "paused"; + if (tab === "error") return status === "error" || status === "terminated"; + return true; +} + function filterAgents(agents: Agent[], tab: FilterTab): Agent[] { - if (tab === "all") return agents; - if (tab === "active") return agents.filter((a) => a.status === "active" || a.status === "running" || a.status === "idle"); - if (tab === "paused") return agents.filter((a) => a.status === "paused"); - if (tab === "error") return agents.filter((a) => a.status === "error" || a.status === "terminated"); - return agents; + return agents.filter((a) => matchesFilter(a.status, tab)); +} + +function filterOrgTree(nodes: OrgNode[], tab: FilterTab): OrgNode[] { + if (tab === "all") return nodes; + return nodes.reduce((acc, node) => { + const filteredReports = filterOrgTree(node.reports, tab); + if (matchesFilter(node.status, tab) || filteredReports.length > 0) { + acc.push({ ...node, reports: filteredReports }); + } + return acc; + }, []); } export function Agents() { @@ -44,7 +59,7 @@ export function Agents() { const { setBreadcrumbs } = useBreadcrumbs(); const navigate = useNavigate(); const [tab, setTab] = useState("all"); - const [view, setView] = useState<"list" | "org">("list"); + const [view, setView] = useState<"list" | "org">("org"); const { data: agents, isLoading, error } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), @@ -58,6 +73,12 @@ export function Agents() { enabled: !!selectedCompanyId && view === "org", }); + const agentMap = useMemo(() => { + const map = new Map(); + for (const a of agents ?? []) map.set(a.id, a); + return map; + }, [agents]); + useEffect(() => { setBreadcrumbs([{ label: "Agents" }]); }, [setBreadcrumbs]); @@ -67,11 +88,22 @@ export function Agents() { } const filtered = filterAgents(agents ?? [], tab); + const filteredOrg = filterOrgTree(orgTree ?? [], tab); return (
-

Agents

+
+

Agents

+ setTab(v as FilterTab)}> + + All{agents ? ` (${agents.length})` : ""} + Active + Paused + Error + + +
{/* View toggle */}
@@ -101,17 +133,6 @@ export function Agents() {
- {view === "list" && ( - setTab(v as FilterTab)}> - - All{agents ? ` (${agents.length})` : ""} - Active - Paused - Error - - - )} - {isLoading &&

Loading...

} {error &&

{error.message}

} @@ -199,14 +220,20 @@ export function Agents() { )} {/* Org chart view */} - {view === "org" && orgTree && orgTree.length > 0 && ( -
- {orgTree.map((node) => ( - + {view === "org" && filteredOrg.length > 0 && ( +
+ {filteredOrg.map((node) => ( + ))}
)} + {view === "org" && orgTree && orgTree.length > 0 && filteredOrg.length === 0 && ( +

+ No agents match the selected filter. +

+ )} + {view === "org" && orgTree && orgTree.length === 0 && (

No organizational hierarchy defined. @@ -220,19 +247,30 @@ function OrgTreeNode({ node, depth, navigate, + agentMap, }: { node: OrgNode; depth: number; navigate: (path: string) => void; + agentMap: Map; }) { + const agent = agentMap.get(node.id); + const statusColor = - node.status === "active" || node.status === "running" - ? "bg-green-400" - : node.status === "paused" - ? "bg-yellow-400" - : node.status === "error" - ? "bg-red-400" - : "bg-neutral-400"; + node.status === "running" + ? "bg-cyan-400 animate-pulse" + : node.status === "active" + ? "bg-green-400" + : node.status === "paused" + ? "bg-yellow-400" + : node.status === "error" + ? "bg-red-400" + : "bg-neutral-400"; + + const budgetPct = + agent && agent.budgetMonthlyCents > 0 + ? Math.round((agent.spentMonthlyCents / agent.budgetMonthlyCents) * 100) + : 0; return (

@@ -247,14 +285,46 @@ function OrgTreeNode({ {node.name} {roleLabels[node.role] ?? node.role} + {agent?.title ? ` - ${agent.title}` : ""}
- +
+ {agent && ( + <> + + {adapterLabels[agent.adapterType] ?? agent.adapterType} + + {agent.lastHeartbeatAt && ( + + {relativeTime(agent.lastHeartbeatAt)} + + )} +
+
+
90 + ? "bg-red-400" + : budgetPct > 70 + ? "bg-yellow-400" + : "bg-green-400" + }`} + style={{ width: `${Math.min(100, budgetPct)}%` }} + /> +
+ + {formatCents(agent.spentMonthlyCents)} / {formatCents(agent.budgetMonthlyCents)} + +
+ + )} + +
{node.reports && node.reports.length > 0 && (
{node.reports.map((child) => ( - + ))}
)}