Compare commits

...

2 Commits

Author SHA1 Message Date
Jiang Bohan
619fb878bf 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
2026-04-09 12:36:21 +08:00
Jiang Bohan
717dcde921 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
2026-04-08 19:08:30 +08:00
2 changed files with 30 additions and 39 deletions

View File

@@ -106,11 +106,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>());
@@ -207,20 +205,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>
)}
</>
);
}
@@ -231,16 +244,14 @@ 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 scrollRef = useRef<HTMLDivElement>(null);
const ignoreScrollRef = useRef(false);
// Elapsed time
useEffect(() => {
@@ -251,20 +262,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) {
@@ -279,10 +276,6 @@ function SingleAgentLiveCard({ task, items, issueId, agentName, scrollContainerR
}, []);
const toggleOpen = useCallback(() => {
if (!open) {
ignoreScrollRef.current = true;
setTimeout(() => { ignoreScrollRef.current = false; }, 300);
}
setOpen(!open);
}, [open]);
@@ -300,7 +293,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"

View File

@@ -994,11 +994,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">