import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import { useUI } from '../context/UIContext'; import { Activity, Server, Cpu, Network, RefreshCw } from 'lucide-react'; import { SpaceParticles } from '../components/SpaceParticles'; import { FixedButton } from '../components/Button'; type SubagentTask = { id: string; status?: string; label?: string; role?: string; agent_id?: string; session_key?: string; memory_ns?: string; tool_allowlist?: string[]; max_retries?: number; retry_count?: number; retry_backoff?: number; max_task_chars?: number; max_result_chars?: number; created?: number; updated?: number; task?: string; result?: string; thread_id?: string; correlation_id?: string; waiting_for_reply?: boolean; }; type RouterReply = { task_id?: string; thread_id?: string; correlation_id?: string; agent_id?: string; status?: string; result?: string; }; type AgentThread = { thread_id?: string; owner?: string; participants?: string[]; status?: string; topic?: string; }; type AgentMessage = { message_id?: string; thread_id?: string; from_agent?: string; to_agent?: string; reply_to?: string; correlation_id?: string; type?: string; content?: string; requires_reply?: boolean; status?: string; created_at?: number; }; type StreamItem = { kind?: 'event' | 'message' | string; at?: number; run_id?: string; agent_id?: string; event_type?: string; message?: string; retry_count?: number; message_id?: string; thread_id?: string; from_agent?: string; to_agent?: string; reply_to?: string; correlation_id?: string; message_type?: string; content?: string; status?: string; requires_reply?: boolean; }; type RegistrySubagent = { agent_id?: string; enabled?: boolean; type?: string; transport?: string; node_id?: string; parent_agent_id?: string; managed_by?: string; notify_main_policy?: string; display_name?: string; role?: string; description?: string; system_prompt_file?: string; prompt_file_found?: boolean; memory_namespace?: string; tool_allowlist?: string[]; inherited_tools?: string[]; effective_tools?: string[]; tool_visibility?: { mode?: string; inherited_tool_count?: number; effective_tool_count?: number; }; routing_keywords?: string[]; }; type AgentTreeNode = { agent_id?: string; display_name?: string; role?: string; type?: string; transport?: string; managed_by?: string; node_id?: string; enabled?: boolean; children?: AgentTreeNode[]; }; type NodeTree = { node_id?: string; node_name?: string; online?: boolean; source?: string; readonly?: boolean; root?: { root?: AgentTreeNode; }; }; type NodeInfo = { id?: string; name?: string; endpoint?: string; version?: string; online?: boolean; }; type AgentTaskStats = { total: number; running: number; failed: number; waiting: number; latestStatus: string; latestUpdated: number; active: Array<{ id: string; status: string; title: string }>; }; type GraphCardSpec = { key: string; branch: string; agentID?: string; transportType?: 'local' | 'remote'; x: number; y: number; w: number; h: number; kind: 'node' | 'agent'; title: string; subtitle: string; meta: string[]; accentTone: 'success' | 'danger' | 'warning' | 'info' | 'accent' | 'neutral'; online?: boolean; clickable?: boolean; highlighted?: boolean; dimmed?: boolean; hidden?: boolean; scale: number; onClick?: () => void; }; function graphAccentBackgroundClass(accentTone: GraphCardSpec['accentTone']) { switch (accentTone) { case 'success': return 'bg-gradient-to-br from-transparent to-emerald-500'; case 'danger': return 'bg-gradient-to-br from-transparent to-red-500'; case 'warning': return 'bg-gradient-to-br from-transparent to-amber-400'; case 'info': return 'bg-gradient-to-br from-transparent to-sky-400'; case 'accent': return 'bg-gradient-to-br from-transparent to-violet-400'; case 'neutral': default: return 'bg-gradient-to-br from-transparent to-zinc-500'; } } function graphAccentIconClass(accentTone: GraphCardSpec['accentTone']) { switch (accentTone) { case 'success': return 'text-emerald-500'; case 'danger': return 'topology-icon-danger'; case 'warning': return 'text-amber-400'; case 'info': return 'text-sky-400'; case 'accent': return 'text-violet-400'; case 'neutral': default: return 'text-zinc-500'; } } type GraphLineSpec = { path: string; dashed?: boolean; branch: string; highlighted?: boolean; dimmed?: boolean; hidden?: boolean; }; type TopologyTooltipState = { title: string; subtitle: string; meta: string[]; x: number; y: number; agentID?: string; transportType?: 'local' | 'remote'; } | null; type StreamPreviewState = { task: SubagentTask | null; items: StreamItem[]; taskID: string; loading?: boolean; }; type TopologyDragState = { active: boolean; startX: number; startY: number; scrollLeft: number; scrollTop: number; }; const cardWidth = 140; const cardHeight = 140; const clusterGap = 60; const sectionGap = 160; const topY = 96; const mainY = 96; const childStartY = 320; function normalizeTitle(value?: string, fallback = '-'): string { const trimmed = `${value || ''}`.trim(); return trimmed || fallback; } function summarizeTask(task?: string, label?: string): string { const text = normalizeTitle(label || task, '-'); return text.length > 52 ? `${text.slice(0, 49)}...` : text; } function formatStreamTime(ts?: number): string { if (!ts) return '--:--:--'; return new Date(ts).toLocaleTimeString([], { hour12: false }); } function formatRuntimeTimestamp(value?: string): string { const raw = `${value || ''}`.trim(); if (!raw || raw === '0001-01-01T00:00:00Z') return '-'; const ts = Date.parse(raw); if (Number.isNaN(ts)) return raw; return new Date(ts).toLocaleString(); } function summarizePreviewText(value?: string, limit = 180): string { const compact = `${value || ''}`.replace(/\s+/g, ' ').trim(); if (!compact) return '(empty)'; return compact.length > limit ? `${compact.slice(0, limit - 3)}...` : compact; } function tokenFromQuery(q: string): string { const raw = String(q || '').trim(); if (!raw) return ''; const search = raw.startsWith('?') ? raw.slice(1) : raw; const params = new URLSearchParams(search); return params.get('token') || ''; } function bezierCurve(x1: number, y1: number, x2: number, y2: number): string { const offset = Math.max(Math.abs(y2 - y1) * 0.5, 60); return `M ${x1} ${y1} C ${x1} ${y1 + offset} ${x2} ${y2 - offset} ${x2} ${y2}`; } function horizontalBezierCurve(x1: number, y1: number, x2: number, y2: number): string { const offset = Math.max(Math.abs(x2 - x1) * 0.5, 60); return `M ${x1} ${y1} C ${x1 + offset} ${y1} ${x2 - offset} ${y2} ${x2} ${y2}`; } function buildTaskStats(tasks: SubagentTask[]): Record { return tasks.reduce>((acc, task) => { const agentID = normalizeTitle(task.agent_id, ''); if (!agentID) return acc; if (!acc[agentID]) { acc[agentID] = { total: 0, running: 0, failed: 0, waiting: 0, latestStatus: '', latestUpdated: 0, active: [] }; } const item = acc[agentID]; item.total += 1; if (task.status === 'running') item.running += 1; if (task.waiting_for_reply) item.waiting += 1; const updatedAt = Math.max(task.updated || 0, task.created || 0); if (updatedAt >= item.latestUpdated) { item.latestUpdated = updatedAt; item.latestStatus = normalizeTitle(task.status, ''); item.failed = task.status === 'failed' ? 1 : 0; } if (task.status === 'running' || task.waiting_for_reply) { item.active.push({ id: task.id, status: task.status || '-', title: summarizeTask(task.task, task.label), }); } return acc; }, {}); } function GraphCard({ card, onHover, onLeave, onDragStart, }: { card: GraphCardSpec; onHover: (card: GraphCardSpec, event: React.MouseEvent) => void; onLeave: () => void; onDragStart: (key: string, event: React.MouseEvent) => void; }) { const isNode = card.kind === 'node'; const Icon = isNode ? Server : Cpu; return (
onDragStart(card.key, e)} onClick={(e) => { if (e.defaultPrevented) return; card.onClick?.(); }} onMouseEnter={(event) => onHover(card, event)} onMouseMove={(event) => onHover(card, event)} onMouseLeave={onLeave} className={`relative w-full h-full rounded-full flex flex-col items-center justify-center gap-1 transition-all duration-300 group ${card.highlighted ? 'scale-[1.05] z-10' : 'hover:scale-[1.02]' }`} style={{ cursor: card.clickable ? 'pointer' : 'default', opacity: card.dimmed ? 0.3 : 1, }} > {/* Sleek Glass Node Background */}
{/* Base dark glass */}
{/* Subtle accent glow */}
{/* Inner depth ring */}
{/* Border ring */}
{/* Content */}
{card.title}
{card.subtitle}
{card.online !== undefined && (
)}
); } const Subagents: React.FC = () => { const { t } = useTranslation(); const { q, nodeTrees, nodeP2P, nodeDispatchItems, subagentRuntimeItems, subagentRegistryItems } = useAppContext(); const ui = useUI(); const [items, setItems] = useState([]); const [selectedId, setSelectedId] = useState(''); const [selectedAgentID, setSelectedAgentID] = useState(''); const [spawnTask, setSpawnTask] = useState(''); const [spawnAgentID, setSpawnAgentID] = useState(''); const [spawnRole, setSpawnRole] = useState(''); const [spawnLabel, setSpawnLabel] = useState(''); const [steerMessage, setSteerMessage] = useState(''); const [dispatchTask, setDispatchTask] = useState(''); const [dispatchAgentID, setDispatchAgentID] = useState(''); const [dispatchRole, setDispatchRole] = useState(''); const [dispatchReply, setDispatchReply] = useState(null); const [dispatchMerged, setDispatchMerged] = useState(''); const [threadDetail, setThreadDetail] = useState(null); const [threadMessages, setThreadMessages] = useState([]); const [inboxMessages, setInboxMessages] = useState([]); const [replyMessage, setReplyMessage] = useState(''); const [replyToMessageID, setReplyToMessageID] = useState(''); const [configAgentID, setConfigAgentID] = useState(''); const [configRole, setConfigRole] = useState(''); const [configDisplayName, setConfigDisplayName] = useState(''); const [configSystemPromptFile, setConfigSystemPromptFile] = useState(''); const [configToolAllowlist, setConfigToolAllowlist] = useState(''); const [configRoutingKeywords, setConfigRoutingKeywords] = useState(''); const [registryItems, setRegistryItems] = useState([]); const [promptFileContent, setPromptFileContent] = useState(''); const [promptFileFound, setPromptFileFound] = useState(false); const [streamPreviewByAgent, setStreamPreviewByAgent] = useState>({}); const [selectedTopologyBranch, setSelectedTopologyBranch] = useState(''); const [topologyFilter, setTopologyFilter] = useState<'all' | 'running' | 'failed' | 'local' | 'remote'>('all'); const [topologyZoom, setTopologyZoom] = useState(0.9); const [topologyPan, setTopologyPan] = useState({ x: 0, y: 0 }); const [nodeOverrides, setNodeOverrides] = useState>({}); const [draggedNode, setDraggedNode] = useState(null); const [topologyTooltip, setTopologyTooltip] = useState(null); const [topologyDragging, setTopologyDragging] = useState(false); const topologyViewportRef = useRef(null); const topologyDragRef = useRef<{ active: boolean; startX: number; startY: number; panX: number; panY: number; }>({ active: false, startX: 0, startY: 0, panX: 0, panY: 0, }); const nodeDragRef = useRef<{ startX: number; startY: number; initialNodeX: number; initialNodeY: number; }>({ startX: 0, startY: 0, initialNodeX: 0, initialNodeY: 0 }); const hasFittedRef = useRef(false); const streamPreviewLoadingRef = useRef>({}); const apiPath = '/webui/api/subagents_runtime'; const withAction = (action: string) => `${apiPath}${q}${q ? '&' : '?'}action=${encodeURIComponent(action)}`; const openAgentStream = (agentID: string, taskID = '', branch = '') => { if (branch) setSelectedTopologyBranch(branch); setSelectedAgentID(agentID); setSelectedId(taskID); }; const load = async () => { try { if (subagentRuntimeItems.length > 0 || subagentRegistryItems.length > 0) { const arr = Array.isArray(subagentRuntimeItems) ? subagentRuntimeItems : []; const registry = Array.isArray(subagentRegistryItems) ? subagentRegistryItems : []; setItems(arr); setRegistryItems(registry); if (registry.length === 0) { setSelectedAgentID(''); setSelectedId(''); } else { const nextAgentID = selectedAgentID && registry.find((x: RegistrySubagent) => x.agent_id === selectedAgentID) ? selectedAgentID : (registry[0]?.agent_id || ''); setSelectedAgentID(nextAgentID); const nextTask = arr.find((x: SubagentTask) => x.agent_id === nextAgentID); setSelectedId(nextTask?.id || ''); } return; } const [tasksRes, registryRes] = await Promise.all([ fetch(withAction('list')), fetch(withAction('registry')), ]); if (!tasksRes.ok) throw new Error(await tasksRes.text()); if (!registryRes.ok) throw new Error(await registryRes.text()); const j = await tasksRes.json(); const registryJson = await registryRes.json(); const arr = Array.isArray(j?.result?.items) ? j.result.items : []; const registryItems = Array.isArray(registryJson?.result?.items) ? registryJson.result.items : []; setItems(arr); setRegistryItems(registryItems); if (registryItems.length === 0) { setSelectedAgentID(''); setSelectedId(''); } else { const nextAgentID = selectedAgentID && registryItems.find((x: RegistrySubagent) => x.agent_id === selectedAgentID) ? selectedAgentID : (registryItems[0]?.agent_id || ''); setSelectedAgentID(nextAgentID); const nextTask = arr.find((x: SubagentTask) => x.agent_id === nextAgentID); setSelectedId(nextTask?.id || ''); } } catch (e) { // Mock data for preview setItems([ { id: 'task-1', status: 'running', agent_id: 'worker-1', role: 'worker', task: 'Process data stream', created: Date.now() } ]); } }; useEffect(() => { load().catch(() => { }); }, [q, selectedAgentID, subagentRuntimeItems, subagentRegistryItems]); const selected = useMemo(() => items.find((x) => x.id === selectedId) || null, [items, selectedId]); const parsedNodeTrees = useMemo(() => { try { const parsed = JSON.parse(nodeTrees); return Array.isArray(parsed) ? parsed : []; } catch { return []; } }, [nodeTrees]); const taskStats = useMemo(() => buildTaskStats(items), [items]); const recentTaskByAgent = useMemo(() => { return items.reduce>((acc, task) => { const agentID = normalizeTitle(task.agent_id, ''); if (!agentID) return acc; const existing = acc[agentID]; const currentScore = Math.max(task.updated || 0, task.created || 0); const existingScore = existing ? Math.max(existing.updated || 0, existing.created || 0) : -1; if (!existing || currentScore > existingScore) { acc[agentID] = task; } return acc; }, {}); }, [items]); const p2pSessionByNode = useMemo(() => { const out: Record = {}; const sessions = Array.isArray(nodeP2P?.nodes) ? nodeP2P.nodes : []; sessions.forEach((session: any) => { const nodeID = normalizeTitle(session?.node, ''); if (!nodeID) return; out[nodeID] = session; }); return out; }, [nodeP2P]); const recentDispatchByNode = useMemo(() => { const out: Record = {}; const rows = Array.isArray(nodeDispatchItems) ? nodeDispatchItems : []; rows.forEach((row: any) => { const nodeID = normalizeTitle(row?.node, ''); if (!nodeID || out[nodeID]) return; out[nodeID] = row; }); return out; }, [nodeDispatchItems]); const topologyGraph = useMemo(() => { const scale = topologyZoom; const originX = 56; const localTree = parsedNodeTrees.find((tree) => normalizeTitle(tree.node_id, '') === 'local') || null; const remoteTrees = parsedNodeTrees.filter((tree) => normalizeTitle(tree.node_id, '') !== 'local'); const localRoot = localTree?.root?.root || { agent_id: 'main', display_name: 'Main Agent', role: 'orchestrator', type: 'router', transport: 'local', enabled: true, children: registryItems .filter((item) => item.agent_id && item.agent_id !== 'main' && item.managed_by === 'config.json') .map((item) => ({ agent_id: item.agent_id, display_name: item.display_name, role: item.role, type: item.type, transport: item.transport, enabled: item.enabled, children: [], })), }; const localChildren = Array.isArray(localRoot.children) ? localRoot.children : []; const localBranchWidth = Math.max(cardWidth, localChildren.length * (cardWidth + clusterGap) - clusterGap); const localOriginX = originX; const localMainX = localOriginX + Math.max(0, (localBranchWidth - cardWidth) / 2); const localMainCenterX = localMainX + cardWidth / 2; const localMainCenterY = mainY + cardHeight / 2; const remoteClusters = remoteTrees.map((tree) => { const root = tree.root?.root; const children = Array.isArray(root?.children) ? root.children : []; return { tree, root, children, width: Math.max(cardWidth, children.length * (cardWidth + clusterGap) - clusterGap), }; }); const totalRemoteWidth = remoteClusters.reduce((sum, cluster, idx) => { return sum + cluster.width + (idx > 0 ? sectionGap : 0); }, 0); const width = Math.max(900, localOriginX * 2 + localBranchWidth + (remoteClusters.length > 0 ? sectionGap + totalRemoteWidth : 0)); const height = childStartY + cardHeight + 40; const cards: GraphCardSpec[] = []; const lines: GraphLineSpec[] = []; const localBranch = 'local'; const localBranchStats = { running: 0, failed: 0, }; const localMainStats = taskStats[normalizeTitle(localRoot.agent_id, 'main')] || { total: 0, running: 0, failed: 0, waiting: 0, latestStatus: '', latestUpdated: 0, active: [] }; const localMainTask = recentTaskByAgent[normalizeTitle(localRoot.agent_id, 'main')]; const localMainRegistry = registryItems.find((item) => item.agent_id === localRoot.agent_id); localBranchStats.running += localMainStats.running; localBranchStats.failed += localMainStats.failed; const localMainCard: GraphCardSpec = { key: 'agent-main', branch: localBranch, agentID: normalizeTitle(localRoot.agent_id, 'main'), transportType: 'local', kind: 'agent', x: localMainX, y: mainY, w: cardWidth, h: cardHeight, title: normalizeTitle(localRoot.display_name, 'Main Agent'), subtitle: `${normalizeTitle(localRoot.agent_id, 'main')} · ${normalizeTitle(localRoot.role, '-')}`, meta: [ `children=${localChildren.length + remoteClusters.length}`, `total=${localMainStats.total} running=${localMainStats.running}`, `waiting=${localMainStats.waiting} failed=${localMainStats.failed}`, `notify=${normalizeTitle(localMainRegistry?.notify_main_policy, 'final_only')}`, `transport=${normalizeTitle(localRoot.transport, 'local')} type=${normalizeTitle(localRoot.type, 'router')}`, `tools=${normalizeTitle(localMainRegistry?.tool_visibility?.mode, 'allowlist')} visible=${localMainRegistry?.tool_visibility?.effective_tool_count ?? 0} inherited=${localMainRegistry?.tool_visibility?.inherited_tool_count ?? 0}`, (localMainRegistry?.inherited_tools || []).length ? `inherits: ${(localMainRegistry?.inherited_tools || []).join(', ')}` : 'inherits: -', localMainStats.active[0] ? `task: ${localMainStats.active[0].title}` : t('noLiveTasks'), ], accentTone: localMainStats.running > 0 ? 'success' : localMainStats.latestStatus === 'failed' ? 'danger' : 'warning', clickable: true, scale, onClick: () => { openAgentStream(normalizeTitle(localRoot.agent_id, 'main'), localMainTask?.id || '', localBranch); }, }; cards.push(localMainCard); localChildren.forEach((child, idx) => { const childX = localOriginX + idx * (cardWidth + clusterGap); const childY = childStartY; const stats = taskStats[normalizeTitle(child.agent_id, '')] || { total: 0, running: 0, failed: 0, waiting: 0, latestStatus: '', latestUpdated: 0, active: [] }; const task = recentTaskByAgent[normalizeTitle(child.agent_id, '')]; const childRegistry = registryItems.find((item) => item.agent_id === child.agent_id); localBranchStats.running += stats.running; localBranchStats.failed += stats.failed; cards.push({ key: `local-child-${child.agent_id || idx}`, branch: localBranch, agentID: normalizeTitle(child.agent_id, ''), transportType: 'local', kind: 'agent', x: childX, y: childY, w: cardWidth, h: cardHeight, title: normalizeTitle(child.display_name, normalizeTitle(child.agent_id, 'agent')), subtitle: `${normalizeTitle(child.agent_id, '-')} · ${normalizeTitle(child.role, '-')}`, meta: [ `total=${stats.total} running=${stats.running}`, `waiting=${stats.waiting} failed=${stats.failed}`, `notify=${normalizeTitle(childRegistry?.notify_main_policy, 'final_only')}`, `transport=${normalizeTitle(child.transport, 'local')} type=${normalizeTitle(child.type, 'worker')}`, `tools=${normalizeTitle(childRegistry?.tool_visibility?.mode, 'allowlist')} visible=${childRegistry?.tool_visibility?.effective_tool_count ?? 0} inherited=${childRegistry?.tool_visibility?.inherited_tool_count ?? 0}`, (childRegistry?.inherited_tools || []).length ? `inherits: ${(childRegistry?.inherited_tools || []).join(', ')}` : 'inherits: -', stats.active[0] ? `task: ${stats.active[0].title}` : task ? `last: ${summarizeTask(task.task, task.label)}` : t('noLiveTasks'), ], accentTone: stats.running > 0 ? 'success' : stats.latestStatus === 'failed' ? 'danger' : 'info', clickable: true, scale, onClick: () => { openAgentStream(normalizeTitle(child.agent_id, ''), task?.id || '', localBranch); }, }); lines.push({ path: bezierCurve(localMainCard.x + cardWidth / 2, localMainCard.y + cardHeight / 2, childX + cardWidth / 2, childY + cardHeight / 2), branch: localBranch, }); }); let remoteOffsetX = localOriginX + localBranchWidth + sectionGap; remoteClusters.forEach((cluster, treeIndex) => { const { tree, root: treeRoot, children } = cluster; const branch = `node:${normalizeTitle(tree.node_id, `remote-${treeIndex}`)}`; const nodeID = normalizeTitle(tree.node_id, ''); const p2pSession = p2pSessionByNode[nodeID]; const recentDispatch = recentDispatchByNode[nodeID]; const rootX = remoteOffsetX + Math.max(0, (cluster.width - cardWidth) / 2); if (!treeRoot) return; const rootCard: GraphCardSpec = { key: `remote-root-${tree.node_id || treeIndex}`, branch, agentID: normalizeTitle(treeRoot.agent_id, ''), transportType: 'remote', kind: 'agent', x: rootX, y: mainY, w: cardWidth, h: cardHeight, title: normalizeTitle(treeRoot.display_name, treeRoot.agent_id || 'main'), subtitle: `${normalizeTitle(treeRoot.agent_id, '-')} · ${normalizeTitle(treeRoot.role, '-')}`, meta: [ `status=${tree.online ? t('online') : t('offline')}`, `transport=${normalizeTitle(treeRoot.transport, 'node')} type=${normalizeTitle(treeRoot.type, 'router')}`, `p2p=${normalizeTitle(nodeP2P?.transport, 'disabled')} session=${normalizeTitle(p2pSession?.status, 'unknown')}`, `last_transport=${normalizeTitle(recentDispatch?.used_transport, '-')}${recentDispatch?.fallback_from ? ` fallback=${normalizeTitle(recentDispatch?.fallback_from, '-')}` : ''}`, `last_ready=${formatRuntimeTimestamp(p2pSession?.last_ready_at)}`, `retry=${Number(p2pSession?.retry_count || 0)}`, `${t('error')}=${normalizeTitle(p2pSession?.last_error, '-')}`, `source=${normalizeTitle(treeRoot.managed_by, tree.source || '-')}`, t('remoteTasksUnavailable'), ], accentTone: !tree.online ? 'neutral' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'open' ? 'success' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'connecting' ? 'warning' : 'accent', clickable: true, scale, onClick: () => { openAgentStream(normalizeTitle(treeRoot.agent_id, ''), '', branch); }, }; cards.push(rootCard); lines.push({ path: horizontalBezierCurve(localMainCard.x + cardWidth / 2, localMainCard.y + cardHeight / 2, rootCard.x + cardWidth / 2, rootCard.y + cardHeight / 2), dashed: true, branch, }); children.forEach((child, idx) => { const childX = remoteOffsetX + idx * (cardWidth + clusterGap); const childY = childStartY; cards.push({ key: `remote-child-${tree.node_id || treeIndex}-${child.agent_id || idx}`, branch, agentID: normalizeTitle(child.agent_id, ''), transportType: 'remote', kind: 'agent', x: childX, y: childY, w: cardWidth, h: cardHeight, title: normalizeTitle(child.display_name, child.agent_id || 'agent'), subtitle: `${normalizeTitle(child.agent_id, '-')} · ${normalizeTitle(child.role, '-')}`, meta: [ `transport=${normalizeTitle(child.transport, 'node')} type=${normalizeTitle(child.type, 'worker')}`, `p2p=${normalizeTitle(nodeP2P?.transport, 'disabled')} session=${normalizeTitle(p2pSession?.status, 'unknown')}`, `last_transport=${normalizeTitle(recentDispatch?.used_transport, '-')}${recentDispatch?.fallback_from ? ` fallback=${normalizeTitle(recentDispatch?.fallback_from, '-')}` : ''}`, `last_ready=${formatRuntimeTimestamp(p2pSession?.last_ready_at)}`, `retry=${Number(p2pSession?.retry_count || 0)}`, `${t('error')}=${normalizeTitle(p2pSession?.last_error, '-')}`, `source=${normalizeTitle(child.managed_by, 'remote_webui')}`, t('remoteTasksUnavailable'), ], accentTone: normalizeTitle(p2pSession?.status, '').toLowerCase() === 'open' ? 'success' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'connecting' ? 'warning' : 'accent', clickable: true, scale, onClick: () => { openAgentStream(normalizeTitle(child.agent_id, ''), '', branch); }, }); lines.push({ path: bezierCurve(rootCard.x + cardWidth / 2, rootCard.y + cardHeight / 2, childX + cardWidth / 2, childY + cardHeight / 2), branch, }); }); remoteOffsetX += cluster.width + sectionGap; }); const highlightedBranch = selectedTopologyBranch.trim(); const branchFilters = new Map(); branchFilters.set(localBranch, topologyFilter === 'all' || topologyFilter === 'local' || (topologyFilter === 'running' && localBranchStats.running > 0) || (topologyFilter === 'failed' && localBranchStats.failed > 0)); remoteTrees.forEach((tree, treeIndex) => { const branch = `node:${normalizeTitle(tree.node_id, `remote-${treeIndex}`)}`; branchFilters.set(branch, topologyFilter === 'all' || topologyFilter === 'remote'); }); const decoratedCards = cards.map((card) => { const override = nodeOverrides[card.key]; return { ...card, x: override ? override.x : card.x, y: override ? override.y : card.y, hidden: branchFilters.get(card.branch) === false, highlighted: !highlightedBranch || card.branch === highlightedBranch, dimmed: branchFilters.get(card.branch) === false ? true : !!highlightedBranch && card.branch !== highlightedBranch, }; }); // Recalculate lines based on potentially overridden card positions const recalculatedLines: GraphLineSpec[] = []; // Helper to find a card's current position const getCardPos = (key: string) => { const c = decoratedCards.find(c => c.key === key); return c ? { cx: c.x + cardWidth / 2, cy: c.y + cardHeight / 2 } : null; }; const localMainPos = getCardPos('agent-main'); localChildren.forEach((child, idx) => { const childKey = `local-child-${child.agent_id || idx}`; const childPos = getCardPos(childKey); if (localMainPos && childPos) { recalculatedLines.push({ path: bezierCurve(localMainPos.cx, localMainPos.cy, childPos.cx, childPos.cy), branch: localBranch, }); } }); remoteClusters.forEach((cluster, treeIndex) => { const branch = `node:${normalizeTitle(cluster.tree.node_id, `remote-${treeIndex}`)}`; const remoteRootKey = `remote-root-${cluster.tree.node_id || treeIndex}`; const remoteRootPos = getCardPos(remoteRootKey); if (localMainPos && remoteRootPos) { recalculatedLines.push({ path: horizontalBezierCurve(localMainPos.cx, localMainPos.cy, remoteRootPos.cx, remoteRootPos.cy), dashed: true, branch, }); } cluster.children.forEach((child, idx) => { const childKey = `remote-child-${cluster.tree.node_id || treeIndex}-${child.agent_id || idx}`; const childPos = getCardPos(childKey); if (remoteRootPos && childPos) { recalculatedLines.push({ path: bezierCurve(remoteRootPos.cx, remoteRootPos.cy, childPos.cx, childPos.cy), branch, }); } }); }); const decoratedLines = recalculatedLines.map((line) => ({ ...line, hidden: branchFilters.get(line.branch) === false, highlighted: !highlightedBranch || line.branch === highlightedBranch, dimmed: branchFilters.get(line.branch) === false ? true : !!highlightedBranch && line.branch !== highlightedBranch, })); return { width, height, cards: decoratedCards, lines: decoratedLines }; }, [parsedNodeTrees, registryItems, taskStats, recentTaskByAgent, selectedTopologyBranch, topologyFilter, t, topologyZoom, nodeOverrides, nodeP2P, p2pSessionByNode, recentDispatchByNode]); const fitView = () => { const viewport = topologyViewportRef.current; if (!viewport || !topologyGraph.width) return; const availableW = viewport.clientWidth; const availableH = viewport.clientHeight; const fitted = Math.min(1.15, Math.max(0.2, (availableW - 48) / topologyGraph.width)); setTopologyZoom(fitted); setTopologyPan({ x: (availableW - topologyGraph.width * fitted) / 2, y: Math.max(24, (availableH - topologyGraph.height * fitted) / 2) }); }; useEffect(() => { if (!hasFittedRef.current && topologyGraph.width > 0) { fitView(); hasFittedRef.current = true; } }, [topologyGraph.width]); useEffect(() => { const viewport = topologyViewportRef.current; if (!viewport) return; const handleWheel = (e: WheelEvent) => { if (!(e.ctrlKey || e.metaKey)) { return; } e.preventDefault(); const rect = viewport.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; setTopologyZoom(prevZoom => { const zoomSensitivity = 0.002; const delta = -e.deltaY * zoomSensitivity; const newZoom = Math.min(Math.max(0.1, prevZoom * (1 + delta)), 4); setTopologyPan(prevPan => { const scaleRatio = newZoom / prevZoom; const newPanX = mouseX - (mouseX - prevPan.x) * scaleRatio; const newPanY = mouseY - (mouseY - prevPan.y) * scaleRatio; return { x: newPanX, y: newPanY }; }); return newZoom; }); }; viewport.addEventListener('wheel', handleWheel, { passive: false }); return () => viewport.removeEventListener('wheel', handleWheel); }, []); const handleTopologyHover = (card: GraphCardSpec, event: React.MouseEvent) => { const tooltipWidth = 360; const tooltipHeight = 420; let x = event.clientX + 14; let y = event.clientY + 14; if (x + tooltipWidth > window.innerWidth) { x = event.clientX - tooltipWidth - 14; } if (y + tooltipHeight > window.innerHeight) { y = event.clientY - tooltipHeight - 14; } setTopologyTooltip({ title: card.title, subtitle: card.subtitle, meta: card.meta, x, y, agentID: card.agentID, transportType: card.transportType, }); }; const clearTopologyTooltip = () => setTopologyTooltip(null); const handleNodeDragStart = (key: string, event: React.MouseEvent) => { if (event.button !== 0) return; event.stopPropagation(); const card = topologyGraph.cards.find(c => c.key === key); if (!card) return; setDraggedNode(key); nodeDragRef.current = { startX: event.clientX, startY: event.clientY, initialNodeX: card.x, initialNodeY: card.y, }; clearTopologyTooltip(); }; const startTopologyDrag = (event: React.MouseEvent) => { if (event.button !== 0) return; topologyDragRef.current = { active: true, startX: event.clientX, startY: event.clientY, panX: topologyPan.x, panY: topologyPan.y, }; setTopologyDragging(true); clearTopologyTooltip(); }; const moveTopologyDrag = (event: React.MouseEvent) => { if (draggedNode) { const deltaX = (event.clientX - nodeDragRef.current.startX) / topologyZoom; const deltaY = (event.clientY - nodeDragRef.current.startY) / topologyZoom; setNodeOverrides(prev => ({ ...prev, [draggedNode]: { x: nodeDragRef.current.initialNodeX + deltaX, y: nodeDragRef.current.initialNodeY + deltaY, } })); return; } if (!topologyDragRef.current.active) return; const deltaX = event.clientX - topologyDragRef.current.startX; const deltaY = event.clientY - topologyDragRef.current.startY; setTopologyPan({ x: topologyDragRef.current.panX + deltaX, y: topologyDragRef.current.panY + deltaY, }); }; const stopTopologyDrag = () => { if (draggedNode) { setDraggedNode(null); } if (topologyDragRef.current.active) { topologyDragRef.current.active = false; setTopologyDragging(false); } }; const callAction = async (payload: Record) => { const r = await fetch(`${apiPath}${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!r.ok) { await ui.notify({ title: t('requestFailed'), message: await r.text() }); return null; } return r.json(); }; const loadThreadAndInbox = async (task: SubagentTask | null) => { if (!task?.id) { setThreadDetail(null); setThreadMessages([]); setInboxMessages([]); return; } try { const [threadRes, inboxRes] = await Promise.all([ callAction({ action: 'thread', id: task.id, limit: 50 }), callAction({ action: 'inbox', id: task.id, limit: 50 }), ]); setThreadDetail(threadRes?.result?.thread || null); setThreadMessages(Array.isArray(threadRes?.result?.messages) ? threadRes.result.messages : []); setInboxMessages(Array.isArray(inboxRes?.result?.messages) ? inboxRes.result.messages : []); } catch (e) { } }; const loadStreamPreview = async (agentID: string, task: SubagentTask | null) => { const taskID = task?.id || ''; if (!agentID) return; if (streamPreviewLoadingRef.current[agentID] === taskID) return; const existing = streamPreviewByAgent[agentID]; if (existing && existing.taskID === taskID && !existing.loading) return; streamPreviewLoadingRef.current[agentID] = taskID; setStreamPreviewByAgent((prev) => ({ ...prev, [agentID]: { task: task || null, items: prev[agentID]?.items || [], taskID, loading: !!taskID, }, })); if (!taskID) { delete streamPreviewLoadingRef.current[agentID]; setStreamPreviewByAgent((prev) => ({ ...prev, [agentID]: { task: null, items: [], taskID: '', loading: false }, })); return; } try { const streamRes = await callAction({ action: 'stream', id: taskID, limit: 12 }); delete streamPreviewLoadingRef.current[agentID]; setStreamPreviewByAgent((prev) => ({ ...prev, [agentID]: { task: streamRes?.result?.task || task, items: Array.isArray(streamRes?.result?.items) ? streamRes.result.items : [], taskID, loading: false, }, })); } catch { delete streamPreviewLoadingRef.current[agentID]; setStreamPreviewByAgent((prev) => ({ ...prev, [agentID]: { task: task || null, items: [], taskID, loading: false }, })); } }; useEffect(() => { const selectedTaskID = String(selected?.id || '').trim(); const previewAgentID = topologyTooltip?.transportType === 'local' ? String(topologyTooltip.agentID || '').trim() : ''; const previewTask = previewAgentID ? recentTaskByAgent[previewAgentID] || null : null; const previewTaskID = String(previewTask?.id || '').trim(); if (!selectedTaskID) { setThreadDetail(null); setThreadMessages([]); setInboxMessages([]); } if (!previewAgentID) { return; } setStreamPreviewByAgent((prev) => ({ ...prev, [previewAgentID]: { task: previewTask, items: prev[previewAgentID]?.items || [], taskID: previewTaskID, loading: !!previewTaskID, }, })); if (!selectedTaskID && !previewTaskID) return; const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const url = new URL(`${proto}//${window.location.host}/webui/api/subagents_runtime/live`); if (tokenFromQuery(q)) url.searchParams.set('token', tokenFromQuery(q)); if (selectedTaskID) url.searchParams.set('task_id', selectedTaskID); if (previewTaskID) url.searchParams.set('preview_task_id', previewTaskID); const ws = new WebSocket(url.toString()); ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); const payload = msg?.payload || {}; if (payload.thread) { setThreadDetail(payload.thread.thread || null); setThreadMessages(Array.isArray(payload.thread.messages) ? payload.thread.messages : []); } if (payload.inbox) { setInboxMessages(Array.isArray(payload.inbox.messages) ? payload.inbox.messages : []); } if (previewAgentID && payload.preview) { setStreamPreviewByAgent((prev) => ({ ...prev, [previewAgentID]: { task: payload.preview.task || previewTask, items: Array.isArray(payload.preview.items) ? payload.preview.items : [], taskID: previewTaskID, loading: false, }, })); } } catch (err) { console.error(err); } }; ws.onerror = () => { if (previewAgentID) { setStreamPreviewByAgent((prev) => ({ ...prev, [previewAgentID]: { task: previewTask, items: prev[previewAgentID]?.items || [], taskID: previewTaskID, loading: false, }, })); } }; return () => { ws.close(); }; }, [selected?.id, topologyTooltip?.agentID, topologyTooltip?.transportType, recentTaskByAgent, q]); return (

{t('subagentsRuntime')}

load()} variant="primary" label={t('refresh')}>
{t('agentTopology')}
{t('agentTopologyHint')}
{(['all', 'running', 'failed', 'local', 'remote'] as const).map((filter) => ( ))} {selectedTopologyBranch && ( )}
{Math.round(topologyZoom * 100)}% · {items.filter((item) => item.status === 'running').length} {t('runningTasks')}
{ stopTopologyDrag(); clearTopologyTooltip(); }} className="radius-canvas relative flex-1 min-h-[420px] sm:min-h-[560px] xl:min-h-[760px] overflow-hidden border border-zinc-800 bg-zinc-950/80" style={{ cursor: topologyDragging ? 'grabbing' : 'grab' }} >
{topologyGraph.lines.map((line, idx) => ( line.hidden ? null : ( {/* Faint energy track */} {/* Flowing light particles */} ) ))} {topologyGraph.cards.map((card) => ( card.hidden ? null : ))}
{topologyTooltip && (
{topologyTooltip.title}
{topologyTooltip.subtitle}
{topologyTooltip.meta.map((line, idx) => { if (!line.includes('=')) { return (
{line}
); } const [key, ...rest] = line.split('='); const value = rest.join('='); return (
{key} {value || '-'}
); })}
{topologyTooltip.transportType === 'local' && topologyTooltip.agentID && (
{t('internalStream')}
{streamPreviewByAgent[topologyTooltip.agentID]?.loading ? (
Loading internal stream...
) : streamPreviewByAgent[topologyTooltip.agentID]?.task ? ( <>
run={streamPreviewByAgent[topologyTooltip.agentID]?.task?.id || '-'}
status={streamPreviewByAgent[topologyTooltip.agentID]?.task?.status || '-'} · thread={streamPreviewByAgent[topologyTooltip.agentID]?.task?.thread_id || '-'}
{streamPreviewByAgent[topologyTooltip.agentID]?.items?.length ? (
{(() => { const item = streamPreviewByAgent[topologyTooltip.agentID].items[streamPreviewByAgent[topologyTooltip.agentID].items.length - 1]; return item.kind === 'event' ? `${item.event_type || 'event'}${item.status ? ` · ${item.status}` : ''}` : `${item.from_agent || '-'} -> ${item.to_agent || '-'} · ${item.message_type || 'message'}`; })()}
{formatStreamTime(streamPreviewByAgent[topologyTooltip.agentID].items[streamPreviewByAgent[topologyTooltip.agentID].items.length - 1]?.at)}
{(() => { const item = streamPreviewByAgent[topologyTooltip.agentID].items[streamPreviewByAgent[topologyTooltip.agentID].items.length - 1]; return summarizePreviewText(item.kind === 'event' ? (item.message || '(no event message)') : (item.content || '(empty message)'), 520); })()}
) : (
No internal stream events yet.
)} ) : (
No persisted run for this agent yet.
)}
)}
)}
); }; export default Subagents;