From 5d1cc2a9bb69109e151c10712ea7fde6aee504e5 Mon Sep 17 00:00:00 2001 From: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:36:43 +0800 Subject: [PATCH] fix(web): multi-agent sticky card with expand/collapse (#516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(web): multi-agent sticky card with expand/collapse pattern - Move sticky positioning to the wrapper div so the entire agent area sticks together instead of each card independently - Show first agent card always visible, with "N more agents working" expand button for additional agents - Remove scrollContainerRef prop (no longer needed with native sticky) - Simplify SingleAgentLiveCard by removing auto-collapse-on-scroll logic * fix(web): pin primary agent card to top and drop collapse UI - Remove the mt-4 wrapper around AgentLiveCard in issue-detail so the sticky wrapper is a direct child of the Activity section — sticky now has a tall enough parent to stay pinned through TaskRunHistory and the full comment timeline - Simplify multi-agent rendering: only the first running agent sticks to the top, any additional agents render below it and scroll with the page. Removes the expand/collapse "N more agents working" button --- .../issues/components/agent-live-card.tsx | 61 ++++++++----------- .../issues/components/issue-detail.tsx | 8 +-- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/apps/web/features/issues/components/agent-live-card.tsx b/apps/web/features/issues/components/agent-live-card.tsx index 620fb8ee8..29363c861 100644 --- a/apps/web/features/issues/components/agent-live-card.tsx +++ b/apps/web/features/issues/components/agent-live-card.tsx @@ -107,11 +107,9 @@ interface TaskState { interface AgentLiveCardProps { issueId: string; - /** Scroll container ref — used to auto-collapse timeline on outer scroll. */ - scrollContainerRef?: React.RefObject; } -export function AgentLiveCard({ issueId, scrollContainerRef }: AgentLiveCardProps) { +export function AgentLiveCard({ issueId }: AgentLiveCardProps) { const { getActorName } = useActorName(); const [taskStates, setTaskStates] = useState>(new Map()); const seenSeqs = useRef(new Set()); @@ -208,20 +206,35 @@ export function AgentLiveCard({ issueId, scrollContainerRef }: AgentLiveCardProp if (taskStates.size === 0) return null; const entries = Array.from(taskStates.values()); + const [firstEntry, ...restEntries] = entries; + if (!firstEntry) return null; return ( -
- {entries.map(({ task, items }) => ( + <> + {/* Primary agent — sticky at top of the Activity section */} +
- ))} -
+
+ {/* Additional agents — scroll with the page */} + {restEntries.length > 0 && ( +
+ {restEntries.map(({ task, items }) => ( + + ))} +
+ )} + ); } @@ -232,17 +245,15 @@ interface SingleAgentLiveCardProps { items: TimelineItem[]; issueId: string; agentName: string; - scrollContainerRef?: React.RefObject; } -function SingleAgentLiveCard({ task, items, issueId, agentName, scrollContainerRef }: SingleAgentLiveCardProps) { +function SingleAgentLiveCard({ task, items, issueId, agentName }: SingleAgentLiveCardProps) { const [elapsed, setElapsed] = useState(""); const [open, setOpen] = useState(false); const [autoScroll, setAutoScroll] = useState(true); const [cancelling, setCancelling] = useState(false); const [transcriptOpen, setTranscriptOpen] = useState(false); const scrollRef = useRef(null); - const ignoreScrollRef = useRef(false); // Elapsed time useEffect(() => { @@ -253,20 +264,6 @@ function SingleAgentLiveCard({ task, items, issueId, agentName, scrollContainerR return () => clearInterval(interval); }, [task.started_at, task.dispatched_at]); - // Auto-collapse timeline when outer scroll container scrolls - useEffect(() => { - const container = scrollContainerRef?.current; - if (!container) return; - - const handleOuterScroll = () => { - if (ignoreScrollRef.current) return; - setOpen(false); - }; - - container.addEventListener("scroll", handleOuterScroll, { passive: true }); - return () => container.removeEventListener("scroll", handleOuterScroll); - }, [scrollContainerRef]); - // Auto-scroll timeline to bottom useEffect(() => { if (autoScroll && scrollRef.current) { @@ -281,10 +278,6 @@ function SingleAgentLiveCard({ task, items, issueId, agentName, scrollContainerR }, []); const toggleOpen = useCallback(() => { - if (!open) { - ignoreScrollRef.current = true; - setTimeout(() => { ignoreScrollRef.current = false; }, 300); - } setOpen(!open); }, [open]); @@ -302,7 +295,7 @@ function SingleAgentLiveCard({ task, items, issueId, agentName, scrollContainerR const toolCount = items.filter((i) => i.type === "tool_use").length; return ( -
+
{/* Header — click to toggle timeline */}
- {/* Agent live output */} - + {/* Agent live output — sticky inside the Activity section so it + stays pinned while scrolling through TaskRunHistory + comments. */} + {/* Agent execution history */}