mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 20:45:37 +02:00
fix(web): multi-agent sticky card with expand/collapse (#516)
* 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
This commit is contained in:
@@ -107,11 +107,9 @@ interface TaskState {
|
||||
|
||||
interface AgentLiveCardProps {
|
||||
issueId: string;
|
||||
/** Scroll container ref — used to auto-collapse timeline on outer scroll. */
|
||||
scrollContainerRef?: React.RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
export function AgentLiveCard({ issueId, scrollContainerRef }: AgentLiveCardProps) {
|
||||
export function AgentLiveCard({ issueId }: AgentLiveCardProps) {
|
||||
const { getActorName } = useActorName();
|
||||
const [taskStates, setTaskStates] = useState<Map<string, TaskState>>(new Map());
|
||||
const seenSeqs = useRef(new Set<string>());
|
||||
@@ -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 (
|
||||
<div className="mt-4 space-y-2">
|
||||
{entries.map(({ task, items }) => (
|
||||
<>
|
||||
{/* Primary agent — sticky at top of the Activity section */}
|
||||
<div className="mt-4 sticky top-4 z-10">
|
||||
<SingleAgentLiveCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
items={items}
|
||||
task={firstEntry.task}
|
||||
items={firstEntry.items}
|
||||
issueId={issueId}
|
||||
agentName={task.agent_id ? getActorName("agent", task.agent_id) : "Agent"}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
agentName={firstEntry.task.agent_id ? getActorName("agent", firstEntry.task.agent_id) : "Agent"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Additional agents — scroll with the page */}
|
||||
{restEntries.length > 0 && (
|
||||
<div className="mt-1.5 space-y-1.5">
|
||||
{restEntries.map(({ task, items }) => (
|
||||
<SingleAgentLiveCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
items={items}
|
||||
issueId={issueId}
|
||||
agentName={task.agent_id ? getActorName("agent", task.agent_id) : "Agent"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -232,17 +245,15 @@ interface SingleAgentLiveCardProps {
|
||||
items: TimelineItem[];
|
||||
issueId: string;
|
||||
agentName: string;
|
||||
scrollContainerRef?: React.RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
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<HTMLDivElement>(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 (
|
||||
<div className="sticky top-4 z-10 rounded-lg border border-info/20 bg-info/5 backdrop-blur-sm">
|
||||
<div className="rounded-lg border border-info/20 bg-info/5 backdrop-blur-sm">
|
||||
{/* Header — click to toggle timeline */}
|
||||
<div
|
||||
className="group flex items-center gap-2 px-3 py-2 cursor-pointer select-none text-muted-foreground hover:text-foreground transition-colors"
|
||||
|
||||
@@ -979,11 +979,9 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent live output */}
|
||||
<AgentLiveCard
|
||||
issueId={id}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
/>
|
||||
{/* Agent live output — sticky inside the Activity section so it
|
||||
stays pinned while scrolling through TaskRunHistory + comments. */}
|
||||
<AgentLiveCard issueId={id} />
|
||||
|
||||
{/* Agent execution history */}
|
||||
<div className="mt-3">
|
||||
|
||||
Reference in New Issue
Block a user