diff --git a/webui/src/pages/Subagents.tsx b/webui/src/pages/Subagents.tsx index e3d30ff..2e3a7ce 100644 --- a/webui/src/pages/Subagents.tsx +++ b/webui/src/pages/Subagents.tsx @@ -178,8 +178,17 @@ type TopologyTooltipState = { 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; @@ -211,6 +220,12 @@ function formatStreamTime(ts?: number): string { return new Date(ts).toLocaleTimeString([], { hour12: false }); } +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 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}`; @@ -336,7 +351,6 @@ const Subagents: React.FC = () => { const [items, setItems] = useState([]); const [selectedId, setSelectedId] = useState(''); const [selectedAgentID, setSelectedAgentID] = useState(''); - const [streamPanelDismissed, setStreamPanelDismissed] = useState(false); const [spawnTask, setSpawnTask] = useState(''); const [spawnAgentID, setSpawnAgentID] = useState(''); const [spawnRole, setSpawnRole] = useState(''); @@ -362,8 +376,7 @@ const Subagents: React.FC = () => { const [registryItems, setRegistryItems] = useState([]); const [promptFileContent, setPromptFileContent] = useState(''); const [promptFileFound, setPromptFileFound] = useState(false); - const [streamItems, setStreamItems] = useState([]); - const [streamTask, setStreamTask] = useState(null); + const [streamPreviewByAgent, setStreamPreviewByAgent] = useState>({}); const [selectedTopologyBranch, setSelectedTopologyBranch] = useState(''); const [topologyFilter, setTopologyFilter] = useState<'all' | 'running' | 'failed' | 'local' | 'remote'>('all'); const [topologyZoom, setTopologyZoom] = useState(0.9); @@ -393,25 +406,17 @@ const Subagents: React.FC = () => { 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 = '') => { - setStreamPanelDismissed(false); if (branch) setSelectedTopologyBranch(branch); setSelectedAgentID(agentID); setSelectedId(taskID); }; - const closeAgentStream = () => { - setStreamPanelDismissed(true); - setSelectedAgentID(''); - setSelectedId(''); - setStreamTask(null); - setStreamItems([]); - }; - const load = async () => { try { const [tasksRes, registryRes] = await Promise.all([ @@ -432,7 +437,7 @@ const Subagents: React.FC = () => { } else { const nextAgentID = selectedAgentID && registryItems.find((x: RegistrySubagent) => x.agent_id === selectedAgentID) ? selectedAgentID - : (streamPanelDismissed ? '' : (registryItems[0]?.agent_id || '')); + : (registryItems[0]?.agent_id || ''); setSelectedAgentID(nextAgentID); const nextTask = arr.find((x: SubagentTask) => x.agent_id === nextAgentID); setSelectedId(nextTask?.id || ''); @@ -447,33 +452,16 @@ const Subagents: React.FC = () => { useEffect(() => { load().catch(() => { }); - }, [q, selectedAgentID, streamPanelDismissed]); + }, [q, selectedAgentID]); useEffect(() => { const timer = window.setInterval(() => { load().catch(() => { }); }, 5000); return () => window.clearInterval(timer); - }, [q, selectedAgentID, streamPanelDismissed]); + }, [q, selectedAgentID]); const selected = useMemo(() => items.find((x) => x.id === selectedId) || null, [items, selectedId]); - const selectedRegistryItem = useMemo( - () => registryItems.find((x) => x.agent_id === selectedAgentID) || null, - [registryItems, selectedAgentID] - ); - const selectedAgentTasks = useMemo( - () => items.filter((x) => x.agent_id === selectedAgentID), - [items, selectedAgentID] - ); - const selectedAgentLatestTask = useMemo( - () => - [...selectedAgentTasks].sort((a, b) => Math.max(b.updated || 0, b.created || 0) - Math.max(a.updated || 0, a.created || 0))[0] || null, - [selectedAgentTasks] - ); - const selectedAgentDisplayName = useMemo( - () => selectedRegistryItem?.display_name || selectedRegistryItem?.agent_id || selectedAgentID || '', - [selectedRegistryItem, selectedAgentID] - ); const parsedNodeTrees = useMemo(() => { try { const parsed = JSON.parse(nodeTrees); @@ -826,8 +814,8 @@ const Subagents: React.FC = () => { }, []); const handleTopologyHover = (card: GraphCardSpec, event: React.MouseEvent) => { - const tooltipWidth = 280; - const tooltipHeight = 160; + const tooltipWidth = 360; + const tooltipHeight = 420; let x = event.clientX + 14; let y = event.clientY + 14; @@ -844,6 +832,8 @@ const Subagents: React.FC = () => { meta: card.meta, x, y, + agentID: card.agentID, + transportType: card.transportType, }); }; @@ -948,25 +938,59 @@ const Subagents: React.FC = () => { loadThreadAndInbox(selected).catch(() => { }); }, [selectedId, q, items]); - const loadStream = async (task: SubagentTask | null) => { - if (!task?.id) { - setStreamTask(null); - setStreamItems([]); + 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: task.id, limit: 100 }); - setStreamTask(streamRes?.result?.task || task); - setStreamItems(Array.isArray(streamRes?.result?.items) ? streamRes.result.items : []); + 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 { - setStreamTask(task); - setStreamItems([]); + delete streamPreviewLoadingRef.current[agentID]; + setStreamPreviewByAgent((prev) => ({ + ...prev, + [agentID]: { task: task || null, items: [], taskID, loading: false }, + })); } }; useEffect(() => { - loadStream(selectedAgentLatestTask).catch(() => { }); - }, [selectedAgentLatestTask?.id, q, items.length]); + if (!topologyTooltip?.agentID || topologyTooltip.transportType !== 'local') return; + const latestTask = recentTaskByAgent[topologyTooltip.agentID] || null; + loadStreamPreview(topologyTooltip.agentID, latestTask).catch(() => { }); + }, [topologyTooltip?.agentID, topologyTooltip?.transportType, recentTaskByAgent, q]); return (
@@ -1150,7 +1174,7 @@ const Subagents: React.FC = () => {
{topologyTooltip && (
@@ -1170,69 +1194,51 @@ const Subagents: React.FC = () => { const [key, ...rest] = line.split('='); const value = rest.join('='); return ( -
+
{key} - {value || '-'} + {value || '-'}
); })}
-
- )} - {selectedAgentID && ( -
event.stopPropagation()} - className="absolute bottom-4 left-4 right-4 z-20 flex h-[46vh] flex-col overflow-hidden border border-zinc-800 brand-card radius-panel shadow-2xl shadow-black/40 backdrop-blur-md md:left-auto md:top-4 md:right-4 md:bottom-4 md:h-auto md:w-[360px] md:max-w-[calc(100%-2rem)] xl:w-[380px]" - > -
-
-
{t('internalStream')}
-
{selectedAgentDisplayName}
-
{selectedAgentID}
-
- -
-
- {streamTask?.id ? ( -
-
run={streamTask.id}
-
status={streamTask.status || '-'} · thread={streamTask.thread_id || '-'}
-
- ) : ( -
No persisted run for this agent yet.
- )} -
-
- {streamItems.length === 0 ? ( -
No internal stream events yet.
- ) : streamItems.map((item, idx) => ( -
-
-
- {item.kind === 'event' - ? `${item.event_type || 'event'}${item.status ? ` · ${item.status}` : ''}` - : `${item.from_agent || '-'} -> ${item.to_agent || '-'} · ${item.message_type || 'message'}`} + {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 || '-'} +
-
{formatStreamTime(item.at)}
-
-
- {item.kind === 'event' ? (item.message || '(no event message)') : (item.content || '(empty message)')} -
-
- {item.kind === 'event' - ? `run=${item.run_id || '-'}${item.retry_count ? ` · retry=${item.retry_count}` : ''}` - : `status=${item.status || '-'}${item.reply_to ? ` · reply_to=${item.reply_to}` : ''}`} -
-
- ))} -
+ {streamPreviewByAgent[topologyTooltip.agentID]?.items?.length ? ( + streamPreviewByAgent[topologyTooltip.agentID].items.slice(-3).reverse().map((item, idx) => ( +
+
+
+ {item.kind === 'event' + ? `${item.event_type || 'event'}${item.status ? ` · ${item.status}` : ''}` + : `${item.from_agent || '-'} -> ${item.to_agent || '-'} · ${item.message_type || 'message'}`} +
+
{formatStreamTime(item.at)}
+
+
+ {summarizePreviewText(item.kind === 'event' ? (item.message || '(no event message)') : (item.content || '(empty message)'))} +
+
+ )) + ) : ( +
No internal stream events yet.
+ )} + + ) : ( +
No persisted run for this agent yet.
+ )} +
+ )}
)}