212 lines
6.9 KiB
TypeScript
212 lines
6.9 KiB
TypeScript
/**
|
|
* Agent Status Widget - shows running agents
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { Bot, Activity, AlertCircle, CheckCircle, Clock } from "lucide-react";
|
|
import type { WidgetProps } from "@mosaic/shared";
|
|
|
|
interface Agent {
|
|
agentId: string;
|
|
taskId: string;
|
|
status: string;
|
|
agentType: string;
|
|
spawnedAt: string;
|
|
completedAt?: string;
|
|
error?: string;
|
|
}
|
|
|
|
export function AgentStatusWidget({ id: _id, config: _config }: WidgetProps): React.JSX.Element {
|
|
const [agents, setAgents] = useState<Agent[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchAgents = useCallback(async (): Promise<void> => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await fetch("/api/orchestrator/agents", {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch agents: ${response.statusText}`);
|
|
}
|
|
|
|
const data = (await response.json()) as Agent[];
|
|
setAgents(data);
|
|
} catch (err: unknown) {
|
|
const errorMessage = err instanceof Error ? err.message : "Unknown error";
|
|
console.error("Failed to fetch agents:", errorMessage);
|
|
setError(errorMessage);
|
|
setAgents([]); // Clear agents on error
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
// Fetch agents from orchestrator API
|
|
useEffect(() => {
|
|
void fetchAgents();
|
|
|
|
// Refresh every 30 seconds
|
|
const interval = setInterval(() => {
|
|
void fetchAgents();
|
|
}, 20000);
|
|
|
|
const eventSource =
|
|
typeof EventSource !== "undefined" ? new EventSource("/api/orchestrator/events") : null;
|
|
if (eventSource) {
|
|
eventSource.onmessage = (): void => {
|
|
void fetchAgents();
|
|
};
|
|
eventSource.onerror = (): void => {
|
|
// polling remains fallback
|
|
};
|
|
}
|
|
|
|
return (): void => {
|
|
clearInterval(interval);
|
|
eventSource?.close();
|
|
};
|
|
}, [fetchAgents]);
|
|
|
|
const getStatusIcon = (status: string): React.JSX.Element => {
|
|
const statusLower = status.toLowerCase();
|
|
switch (statusLower) {
|
|
case "running":
|
|
case "working":
|
|
return <Activity className="w-4 h-4 text-blue-500 animate-pulse" />;
|
|
case "spawning":
|
|
case "queued":
|
|
return <Clock className="w-4 h-4 text-yellow-500" />;
|
|
case "completed":
|
|
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
|
case "failed":
|
|
case "error":
|
|
return <AlertCircle className="w-4 h-4 text-red-500" />;
|
|
case "terminated":
|
|
case "killed":
|
|
return <CheckCircle className="w-4 h-4 text-gray-500" />;
|
|
default:
|
|
return <Clock className="w-4 h-4 text-gray-400" />;
|
|
}
|
|
};
|
|
|
|
const getStatusText = (status: string): string => {
|
|
return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
|
|
};
|
|
|
|
const getAgentName = (agent: Agent): string => {
|
|
const typeMap: Record<string, string> = {
|
|
worker: "Worker Agent",
|
|
reviewer: "Code Review Agent",
|
|
tester: "Test Runner Agent",
|
|
};
|
|
return typeMap[agent.agentType] ?? `${getStatusText(agent.agentType)} Agent`;
|
|
};
|
|
|
|
const getTimeSinceSpawn = (timestamp: string): string => {
|
|
const now = new Date();
|
|
const spawned = new Date(timestamp);
|
|
const diffMs = now.getTime() - spawned.getTime();
|
|
|
|
if (diffMs < 60000) return "Just now";
|
|
if (diffMs < 3600000) return `${String(Math.floor(diffMs / 60000))}m ago`;
|
|
if (diffMs < 86400000) return `${String(Math.floor(diffMs / 3600000))}h ago`;
|
|
return `${String(Math.floor(diffMs / 86400000))}d ago`;
|
|
};
|
|
|
|
const stats = {
|
|
total: agents.length,
|
|
working: agents.filter((a) => a.status.toLowerCase() === "running").length,
|
|
idle: agents.filter((a) => a.status.toLowerCase() === "spawning").length,
|
|
error: agents.filter((a) => a.status.toLowerCase() === "failed").length,
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-gray-500 text-sm">Loading agents...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-red-500 text-sm">
|
|
<AlertCircle className="w-4 h-4 inline mr-1" />
|
|
{error}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full space-y-3">
|
|
{/* Summary stats */}
|
|
<div className="grid grid-cols-4 gap-1 text-center text-xs">
|
|
<div className="bg-gray-50 rounded p-2">
|
|
<div className="text-lg font-bold text-gray-900">{stats.total}</div>
|
|
<div className="text-gray-500">Total</div>
|
|
</div>
|
|
<div className="bg-blue-50 rounded p-2">
|
|
<div className="text-lg font-bold text-blue-600">{stats.working}</div>
|
|
<div className="text-blue-500">Working</div>
|
|
</div>
|
|
<div className="bg-gray-100 rounded p-2">
|
|
<div className="text-lg font-bold text-gray-600">{stats.idle}</div>
|
|
<div className="text-gray-500">Idle</div>
|
|
</div>
|
|
<div className="bg-red-50 rounded p-2">
|
|
<div className="text-lg font-bold text-red-600">{stats.error}</div>
|
|
<div className="text-red-500">Error</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Agent list */}
|
|
<div className="flex-1 overflow-auto space-y-2">
|
|
{agents.length === 0 ? (
|
|
<div className="text-center text-gray-500 text-sm py-4">No agents running</div>
|
|
) : (
|
|
agents.map((agent) => (
|
|
<div
|
|
key={agent.agentId}
|
|
className={`p-3 rounded-lg border ${
|
|
agent.status.toLowerCase() === "failed"
|
|
? "bg-red-50 border-red-200"
|
|
: agent.status.toLowerCase() === "running"
|
|
? "bg-blue-50 border-blue-200"
|
|
: "bg-gray-50 border-gray-200"
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<Bot className="w-4 h-4 text-gray-600" />
|
|
<span className="text-sm font-medium text-gray-900">{getAgentName(agent)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1 text-xs text-gray-500">
|
|
{getStatusIcon(agent.status)}
|
|
<span>{getStatusText(agent.status)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-xs text-gray-600 mb-1">Task: {agent.taskId}</div>
|
|
|
|
{agent.error && <div className="text-xs text-red-600 mb-1">{agent.error}</div>}
|
|
|
|
<div className="flex items-center justify-between text-xs text-gray-400">
|
|
<span>Agent ID: {agent.agentId.slice(0, 8)}...</span>
|
|
<span>{getTimeSinceSpawn(agent.spawnedAt)}</span>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|