import { IconPlus, IconPencil, IconTrash, IconX } from "@tabler/icons-react"; import { useState, useEffect, useRef, useCallback } from "react"; import { getRemoteAgentIdFromPath, isRemoteAgentPath, remoteAgentResourcePath, } from "../../resources/metadata.js"; import { agentNativePath } from "../api-path.js"; import { Tooltip, TooltipContent, TooltipTrigger, } from "../components/ui/tooltip.js"; interface AgentInfo { id: string; path: string; name: string; url: string; description?: string; } function AgentEditPopover({ agent, onSave, onDelete, onClose, }: { agent: AgentInfo; onSave: (agent: AgentInfo) => void; onDelete: (id: string) => void; onClose: () => void; }) { const [name, setName] = useState(agent.name); const [url, setUrl] = useState(agent.url); const [description, setDescription] = useState(agent.description ?? ""); const popoverRef = useRef(null); useEffect(() => { function handleClick(e: MouseEvent) { if ( popoverRef.current && !popoverRef.current.contains(e.target as Node) ) { onClose(); } } document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [onClose]); const handleSave = () => { if (!name.trim() || !url.trim()) return; onSave({ ...agent, name: name.trim(), url: url.trim(), description: description.trim() || undefined, }); }; return (
setName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSave(); if (e.key === "Escape") onClose(); }} className="w-full rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent" placeholder="Name" /> setUrl(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSave(); if (e.key === "Escape") onClose(); }} className="w-full rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent" placeholder="URL (e.g. http://localhost:8085)" /> setDescription(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSave(); if (e.key === "Escape") onClose(); }} className="w-full rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent" placeholder="Description (optional)" />
); } function AgentAddPopover({ onAdd, onClose, }: { onAdd: (name: string, url: string, description: string) => void; onClose: () => void; }) { const [name, setName] = useState(""); const [url, setUrl] = useState(""); const [description, setDescription] = useState(""); const nameRef = useRef(null); const popoverRef = useRef(null); useEffect(() => { const t = setTimeout(() => nameRef.current?.focus(), 50); return () => clearTimeout(t); }, []); useEffect(() => { function handleClick(e: MouseEvent) { if ( popoverRef.current && !popoverRef.current.contains(e.target as Node) ) { onClose(); } } document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [onClose]); const handleAdd = () => { if (!name.trim() || !url.trim()) return; onAdd(name.trim(), url.trim(), description.trim()); }; return (
setName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleAdd(); if (e.key === "Escape") onClose(); }} className="w-full rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent" placeholder="Name" /> setUrl(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleAdd(); if (e.key === "Escape") onClose(); }} className="w-full rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent" placeholder="URL (e.g. http://localhost:8085)" /> setDescription(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleAdd(); if (e.key === "Escape") onClose(); }} className="w-full rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent" placeholder="Description (optional)" />
); } export function AgentsSection() { const [agents, setAgents] = useState([]); const [loading, setLoading] = useState(true); const [editingAgent, setEditingAgent] = useState(null); const [showAdd, setShowAdd] = useState(false); const fetchAgents = useCallback(async () => { try { const res = await fetch( agentNativePath("/_agent-native/resources?scope=all"), ); if (!res.ok) return; const data = await res.json(); const agentResources = (data.resources ?? []).filter( (r: { path: string }) => isRemoteAgentPath(r.path), ); const parsed = await Promise.all( agentResources.map(async (r: { id: string; path: string }) => { try { const detail = await fetch( agentNativePath(`/_agent-native/resources/${r.id}`), ); if (!detail.ok) return null; const d = await detail.json(); const config = JSON.parse(d.content); return { id: r.id, path: r.path, name: config.name, url: config.url, description: config.description, }; } catch { return null; } }), ); setAgents(parsed.filter(Boolean)); } finally { setLoading(false); } }, []); useEffect(() => { fetchAgents(); }, [fetchAgents]); const handleAdd = async (name: string, url: string, description: string) => { const id = name.toLowerCase().replace(/[^a-z0-9-]/g, "-"); const agentJson = JSON.stringify( { id, name, description: description || undefined, url, color: "#6B7280", }, null, 2, ); try { const res = await fetch(agentNativePath("/_agent-native/resources"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: remoteAgentResourcePath(id), content: agentJson, shared: true, }), }); if (res.ok) { setShowAdd(false); fetchAgents(); } } catch {} }; const handleSave = async (agent: AgentInfo) => { const agentJson = JSON.stringify( { id: getRemoteAgentIdFromPath(agent.path), name: agent.name, description: agent.description || undefined, url: agent.url, color: "#6B7280", }, null, 2, ); try { const res = await fetch( agentNativePath(`/_agent-native/resources/${agent.id}`), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: agentJson }), }, ); if (res.ok) { setEditingAgent(null); fetchAgents(); } } catch {} }; const handleDelete = async (agentId: string) => { try { const res = await fetch( agentNativePath(`/_agent-native/resources/${agentId}`), { method: "DELETE", headers: { "Content-Type": "application/json" }, }, ); if (res.ok) { setEditingAgent(null); fetchAgents(); } } catch {} }; return (
{/* Header with + button */}
@-mention agents in chat to delegate tasks via A2A.
Add agent {showAdd && ( setShowAdd(false)} /> )}
{/* Agent list */} {loading ? (
) : agents.length === 0 ? ( ) : (
{agents.map((agent) => (
{agent.name} {agent.url} Edit agent
{editingAgent === agent.id && ( setEditingAgent(null)} /> )}
))}
)}
); }