From cec9e3b42bbce0a2f6a3a98bbb2986bf4e88f2ce Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 4 Feb 2026 11:23:49 +0700 Subject: [PATCH] =?UTF-8?q?Fix=20user=E2=86=92tool=20call=20spacing=20in?= =?UTF-8?q?=20timeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/src/components/agent-stream-view.tsx | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 676ae3e08..8374a7a8b 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -212,37 +212,46 @@ export function AgentStreamView({ const tightGap = theme.spacing[1]; // 4px const looseGap = theme.spacing[4]; // 16px - // In inverted lists, marginBottom renders as visual gap ABOVE the item. - // Index 0 is visually at the bottom, so use the item ABOVE (index + 1) to compute spacing. - const getGapAbove = useCallback( + // The FlatList is inverted, but each row is re-inverted by RN, so `marginBottom` still + // manifests as the gap *below* the row in the final visual layout. Compute spacing + // against the item visually below (index - 1 in our inverted list). + const getGapBelow = useCallback( (item: StreamItem, index: number) => { - const aboveItem = flatListData[index + 1]; - if (!aboveItem) { - // This is the topmost item; nothing above it visually. + const belowItem = flatListData[index - 1]; + if (!belowItem) { + // This is the bottommost (newest) item; nothing below it visually. return 0; } // Same type groups get tight gap (4px) - if (isUserMessageItem(item) && isUserMessageItem(aboveItem)) { + if (isUserMessageItem(item) && isUserMessageItem(belowItem)) { return tightGap; } - if (isToolSequenceItem(item) && isToolSequenceItem(aboveItem)) { + if (isToolSequenceItem(item) && isToolSequenceItem(belowItem)) { return tightGap; } - // Keep tool sequences visually connected to the preceding user message / tasks. + // Give user messages more breathing room before tool sequences. + if (item.kind === "user_message" && isToolSequenceItem(belowItem)) { + return looseGap; + } + + // Keep tool sequences visually connected to the preceding user/assistant message. if ( - isToolSequenceItem(item) && - (aboveItem.kind === "user_message" || - aboveItem.kind === "assistant_message" || - aboveItem.kind === "todo_list") + (item.kind === "user_message" || item.kind === "assistant_message") && + isToolSequenceItem(belowItem) ) { return tightGap; } - // And keep assistant messages visually connected to tool sequences (symmetry). - if (item.kind === "assistant_message" && isToolSequenceItem(aboveItem)) { + // Keep todo lists visually connected to the following tool sequence (symmetry). + if (item.kind === "todo_list" && isToolSequenceItem(belowItem)) { + return tightGap; + } + + // Keep tool sequences visually connected to the assistant response (symmetry). + if (isToolSequenceItem(item) && belowItem.kind === "assistant_message") { return tightGap; } @@ -380,7 +389,7 @@ export function AgentStreamView({ return null; } - const gapAbove = getGapAbove(item, index); + const gapBelow = getGapBelow(item, index); // Check if this is the end of a turn (before a user message or end of stream when not running) // In inverted list: index-1 is the next item (newer in time) @@ -393,14 +402,14 @@ export function AgentStreamView({ const getContent = () => collectTurnContent(index); return ( - + {content} {isEndOfTurn ? : null} ); }, [ - getGapAbove, + getGapBelow, renderStreamItemContent, flatListData, agent.status,