import * as React from "react"; import { useTranslation } from "react-i18next"; import { Bot, RefreshCw, Search, Sparkles, Trash2, } from "lucide-react"; import { createAgentConfig, deleteAgent, listAgentConfigs, listAgentRuns, listModels, listSkills, updateAgent } from "@/api"; import { translateApiError } from "@/api/errors"; import { ApiErrorState } from "@/components/shared/ApiErrorState"; import { ConfirmDialog } from "@/components/shared/ConfirmDialog"; import { EmptyState } from "@/components/shared/EmptyState"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { PageHeader } from "@/components/shared/PageHeader"; import { SearchInput } from "@/components/shared/SearchInput"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog } from "@/components/ui/dialog"; import { Input, Textarea } from "@/components/ui/input"; import { Select } from "@/components/ui/select"; import { toast } from "@/components/ui/toaster"; import { demoText } from "@/lib/demo-text"; import { useAgentList } from "@/hooks"; import type { AgentConfig, AgentDefinition, AgentRun, JSONObject, ModelDefinition, SkillDefinition } from "@/types"; import { AgentOverview } from "./components/AgentOverview"; import { AgentRuns } from "./components/AgentRuns"; import { CreateAgentDialog } from "./components/CreateAgentDialog"; export function AgentListPage() { const { t } = useTranslation(); const [search, setSearch] = React.useState(""); const [selectedAgentId, setSelectedAgentId] = React.useState(); const [configs, setConfigs] = React.useState([]); const [runs, setRuns] = React.useState([]); const [skills, setSkills] = React.useState([]); const [relatedLoading, setRelatedLoading] = React.useState(true); const [directoryLoading, setDirectoryLoading] = React.useState(false); const [editOpen, setEditOpen] = React.useState(false); const [deleteOpen, setDeleteOpen] = React.useState(false); const [detailTab, setDetailTab] = React.useState<"overview" | "test">("overview"); const agents = useAgentList(); const agentList = agents.data ?? []; const selectedAgent = agentList.find((agent) => agent.id === selectedAgentId) ?? agentList[0]; const selectedConfigs = configs.filter((config) => config.agent_id === selectedAgent?.id); const selectedRuns = runs .filter((r) => r.agent_id === selectedAgent?.id) .sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime()); const activeConfig = [...selectedConfigs].sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime())[0]; const configByAgent = React.useMemo(() => { const grouped = new Map(); configs.forEach((config) => { grouped.set(config.agent_id, [...(grouped.get(config.agent_id) ?? []), config]); }); const result = new Map(); grouped.forEach((items, agentId) => { const config = [...items].sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime())[0]; if (config) result.set(agentId, config); }); return result; }, [configs]); const runsByAgent = React.useMemo(() => { const result = new Map(); runs.forEach((run) => { result.set(run.agent_id, [...(result.get(run.agent_id) ?? []), run]); }); return result; }, [runs]); const filtered = agentList.filter((agent) => { const text = `${agent.name} ${agent.description ?? ""}`.toLowerCase(); return text.includes(search.toLowerCase()); }); const loadSkills = React.useCallback(async () => { try { setSkills(await listSkills()); } catch { setSkills([]); } }, []); const loadRelated = React.useCallback(async (agentId?: string, options?: { silent?: boolean }) => { if (!agentId) { setConfigs([]); setRuns([]); setRelatedLoading(false); return; } if (!options?.silent) setRelatedLoading(true); try { const [configData, runData] = await Promise.all([listAgentConfigs(agentId), listAgentRuns(agentId)]); setConfigs((current) => [...current.filter((config) => config.agent_id !== agentId), ...configData]); setRuns((current) => [...current.filter((run) => run.agent_id !== agentId), ...runData]); } catch { toast.error(t("errors.failedToLoad")); } finally { if (!options?.silent) setRelatedLoading(false); } }, [t]); const loadDirectorySummaries = React.useCallback(async (agentIds: string[]) => { if (!agentIds.length) return; setDirectoryLoading(true); try { const [configResults, runResults] = await Promise.all([ Promise.allSettled(agentIds.map((agentId) => listAgentConfigs(agentId))), Promise.allSettled(agentIds.map((agentId) => listAgentRuns(agentId))), ]); const nextConfigs = configResults.flatMap((result) => result.status === "fulfilled" ? result.value : []); const nextRuns = runResults.flatMap((result) => result.status === "fulfilled" ? result.value : []); setConfigs((current) => [ ...current.filter((config) => !agentIds.includes(config.agent_id)), ...nextConfigs, ]); setRuns((current) => [ ...current.filter((run) => !agentIds.includes(run.agent_id)), ...nextRuns, ]); } finally { setDirectoryLoading(false); } }, []); React.useEffect(() => { void loadSkills(); }, [loadSkills]); React.useEffect(() => { void loadDirectorySummaries(agentList.map((agent) => agent.id)); }, [agentList, loadDirectorySummaries]); React.useEffect(() => { void loadRelated(selectedAgent?.id); }, [loadRelated, selectedAgent?.id]); React.useEffect(() => { if (!selectedAgentId && agentList[0]) setSelectedAgentId(agentList[0].id); }, [agentList, selectedAgentId]); React.useEffect(() => { setDetailTab("overview"); }, [selectedAgent?.id]); async function handleDelete() { if (!selectedAgent) return; try { await deleteAgent(selectedAgent.id); toast.success(t("agents.agentDeleted")); setSelectedAgentId(undefined); setDeleteOpen(false); void agents.refetch(); } catch { toast.error(t("agents.failedToDeleteAgent")); } } if (agents.loading) return ; if (agents.error) return void agents.refetch()} />; return (
void agents.refetch()} /> } />
{t("agents.agentDirectory")} {directoryLoading ? t("common.loading") : t("agents.shown", { shown: filtered.length, total: agentList.length })}
{agentList.length}
{filtered.length ? (
{filtered.map((agent) => ( setSelectedAgentId(agent.id)} /> ))}
) : ( )}
{selectedAgent ? demoText(selectedAgent.name, t) : t("agents.agentDetails")}
{selectedAgent ? t("agents.manageDescription") : t("agents.selectAgent")}
{selectedAgent && (
)}
{selectedAgent ? (
{detailTab === "overview" ? ( r.status === "failed").length} /> ) : ( void loadRelated(selectedAgent.id, { silent: true })} /> )}
) : ( )}
{selectedAgent && ( { void agents.refetch(); void loadRelated(selectedAgent.id); }} /> )}
); } function AgentDirectoryRow({ agent, config, active, runs, onClick, }: { agent: AgentDefinition; config?: AgentConfig; active: boolean; runs: AgentRun[]; onClick: () => void; }) { const { t } = useTranslation(); const model = config ? formatAgentModel(config, t) : t("agents.noModelSelected"); const provider = config ? formatAgentProvider(config, t) : t("agents.providerNotSet"); const skillCount = config?.skill_refs_json.length ?? 0; const failedCount = runs.filter((run) => run.status === "failed").length; const latestRun = [...runs].sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime())[0]; return ( ); } function formatAgentModel(config: AgentConfig, t: ReturnType["t"]) { const value = config.model_config_json.model; return typeof value === "string" && value ? value : t("agents.noModelSelected"); } function formatAgentProvider(config: AgentConfig, t: ReturnType["t"]) { const value = config.model_config_json.provider; return typeof value === "string" && value ? readableAgentValue(value, t) : t("agents.providerNotSet"); } function readableAgentValue(value: string, t: ReturnType["t"]) { const fallback = value.split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" "); return t(`agents.valueLabels.${value}`, fallback); } function EditAgentDialog({ agent, activeConfig, open, onOpenChange, onSaved, }: { agent: AgentDefinition; activeConfig?: AgentConfig; open: boolean; onOpenChange: (open: boolean) => void; onSaved: () => void; }) { const { t } = useTranslation(); const [name, setName] = React.useState(agent.name); const [systemPrompt, setSystemPrompt] = React.useState(activeConfig?.system_prompt ?? ""); const [models, setModels] = React.useState([]); const [selectedModelId, setSelectedModelId] = React.useState(""); const [availableSkills, setAvailableSkills] = React.useState([]); const [selectedSkillIds, setSelectedSkillIds] = React.useState([]); const [skillsLoading, setSkillsLoading] = React.useState(false); const [skillsError, setSkillsError] = React.useState(null); const [memoryScope, setMemoryScope] = React.useState("session"); const [temperature, setTemperature] = React.useState("0.7"); const [maxTokens, setMaxTokens] = React.useState("4096"); const [timeoutSeconds, setTimeoutSeconds] = React.useState("60"); const [retryAttempts, setRetryAttempts] = React.useState("2"); const [retryBackoffMs, setRetryBackoffMs] = React.useState("800"); const [toolCallLimit, setToolCallLimit] = React.useState("8"); const [contextWindow, setContextWindow] = React.useState(""); const [outputFormat, setOutputFormat] = React.useState("text"); const [humanApprovalPolicy, setHumanApprovalPolicy] = React.useState("never"); const [submitting, setSubmitting] = React.useState(false); const currentModel = models.find((model) => model.id === selectedModelId); const modelOptions = React.useMemo(() => { return models .filter((model) => { const capabilities = model.capabilities_json ?? []; return capabilities.length === 0 || capabilities.includes("chat") || capabilities.includes("reasoning"); }) .map((model) => ({ value: model.id, label: `${model.name} - ${model.model_name}`, })); }, [models]); React.useEffect(() => { if (!open) return; setName(agent.name); hydrateFromConfig(activeConfig); void listModels().then((items) => { setModels(items); hydrateModelSelection(items, activeConfig); }); setSkillsLoading(true); setSkillsError(null); void listSkills() .then((skills) => setAvailableSkills(Array.isArray(skills) ? skills : [])) .catch((err) => { setAvailableSkills([]); setSkillsError(translateApiError(err)); }) .finally(() => setSkillsLoading(false)); }, [open, agent, activeConfig]); function hydrateFromConfig(config?: AgentConfig) { const modelConfig = config?.model_config_json ?? {}; const memoryPolicy = config?.memory_policy_json ?? {}; const runtimePolicy = config?.runtime_policy_json ?? {}; const retryPolicy = getRecord(runtimePolicy, "retry_policy") ?? {}; setSystemPrompt(config?.system_prompt ?? ""); setSelectedSkillIds((config?.skill_refs_json ?? []).flatMap((item) => { const skillId = getString(item, "skill_id"); return skillId ? [skillId] : []; })); setMemoryScope(getString(memoryPolicy, "memory_scope") ?? "session"); setTemperature(getConfigString(modelConfig, "temperature", "0.7")); setMaxTokens(getConfigString(modelConfig, "max_tokens", "4096")); setTimeoutSeconds(getConfigString(modelConfig, "timeout_seconds", "60")); setContextWindow(getConfigString(modelConfig, "context_window", "")); setOutputFormat(getString(modelConfig, "output_format") ?? "text"); setRetryAttempts(getConfigString(retryPolicy, "max_attempts", "2")); setRetryBackoffMs(getConfigString(retryPolicy, "backoff_ms", "800")); setToolCallLimit(getConfigString(runtimePolicy, "tool_call_limit", "8")); setHumanApprovalPolicy(getString(runtimePolicy, "human_approval_policy") ?? "never"); } function hydrateModelSelection(items: ModelDefinition[], config?: AgentConfig) { const modelConfig = config?.model_config_json ?? {}; const modelDefinitionId = getString(modelConfig, "model_id"); const providerType = getString(modelConfig, "provider"); const modelName = getString(modelConfig, "model"); const matchedModel = items.find((model) => model.id === modelDefinitionId || (model.provider_type === providerType && model.model_name === modelName) ); if (matchedModel) { setSelectedModelId(matchedModel.id); return; } const firstModel = items.find((model) => { const capabilities = model.capabilities_json ?? []; return capabilities.length === 0 || capabilities.includes("chat") || capabilities.includes("reasoning"); }); setSelectedModelId(firstModel?.id ?? ""); } function toggleSkill(skillId: string) { setSelectedSkillIds((current) => current.includes(skillId) ? current.filter((id) => id !== skillId) : [...current, skillId] ); } async function submit(event: React.FormEvent) { event.preventDefault(); setSubmitting(true); try { await updateAgent(agent.id, { name: name.trim() }); await createAgentConfig({ agent_id: agent.id, role: "assistant", system_prompt: systemPrompt, model_config_json: { model_id: currentModel?.id ?? null, provider: currentModel?.provider_type ?? getString(activeConfig?.model_config_json ?? {}, "provider") ?? "openai", model: currentModel?.model_name ?? getString(activeConfig?.model_config_json ?? {}, "model") ?? "", temperature: parseOptionalFloat(temperature) ?? 0.7, max_tokens: parseOptionalInteger(maxTokens) ?? 4096, timeout_seconds: parseOptionalInteger(timeoutSeconds) ?? 60, context_window: parseOptionalInteger(contextWindow), output_format: outputFormat, }, memory_policy_json: { memory_scope: memoryScope, }, runtime_policy_json: { retry_policy: { max_attempts: parseOptionalInteger(retryAttempts) ?? 2, backoff_ms: parseOptionalInteger(retryBackoffMs) ?? 800, }, tool_call_limit: parseOptionalInteger(toolCallLimit) ?? 8, human_approval_policy: humanApprovalPolicy, }, tool_refs_json: activeConfig?.tool_refs_json ?? [], skill_refs_json: selectedSkillIds.map((skillId) => ({ skill_id: skillId })), }); toast.success(t("agents.agentUpdated")); onOpenChange(false); onSaved(); } catch { toast.error(t("agents.failedToUpdateAgent")); } finally { setSubmitting(false); } } return (

{t("agents.basicAgent")}

setName(event.target.value)} placeholder={t("agents.namePlaceholder")} />