Fix user→tool call spacing in timeline

This commit is contained in:
Mohamed Boudra
2026-02-04 11:23:49 +07:00
parent 6fbc09de2c
commit cec9e3b42b
@@ -212,37 +212,46 @@ export function AgentStreamView({
const tightGap = theme.spacing[1]; // 4px const tightGap = theme.spacing[1]; // 4px
const looseGap = theme.spacing[4]; // 16px const looseGap = theme.spacing[4]; // 16px
// In inverted lists, marginBottom renders as visual gap ABOVE the item. // The FlatList is inverted, but each row is re-inverted by RN, so `marginBottom` still
// Index 0 is visually at the bottom, so use the item ABOVE (index + 1) to compute spacing. // manifests as the gap *below* the row in the final visual layout. Compute spacing
const getGapAbove = useCallback( // against the item visually below (index - 1 in our inverted list).
const getGapBelow = useCallback(
(item: StreamItem, index: number) => { (item: StreamItem, index: number) => {
const aboveItem = flatListData[index + 1]; const belowItem = flatListData[index - 1];
if (!aboveItem) { if (!belowItem) {
// This is the topmost item; nothing above it visually. // This is the bottommost (newest) item; nothing below it visually.
return 0; return 0;
} }
// Same type groups get tight gap (4px) // Same type groups get tight gap (4px)
if (isUserMessageItem(item) && isUserMessageItem(aboveItem)) { if (isUserMessageItem(item) && isUserMessageItem(belowItem)) {
return tightGap; return tightGap;
} }
if (isToolSequenceItem(item) && isToolSequenceItem(aboveItem)) { if (isToolSequenceItem(item) && isToolSequenceItem(belowItem)) {
return tightGap; 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 ( if (
isToolSequenceItem(item) && (item.kind === "user_message" || item.kind === "assistant_message") &&
(aboveItem.kind === "user_message" || isToolSequenceItem(belowItem)
aboveItem.kind === "assistant_message" ||
aboveItem.kind === "todo_list")
) { ) {
return tightGap; return tightGap;
} }
// And keep assistant messages visually connected to tool sequences (symmetry). // Keep todo lists visually connected to the following tool sequence (symmetry).
if (item.kind === "assistant_message" && isToolSequenceItem(aboveItem)) { 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; return tightGap;
} }
@@ -380,7 +389,7 @@ export function AgentStreamView({
return null; 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) // 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) // In inverted list: index-1 is the next item (newer in time)
@@ -393,14 +402,14 @@ export function AgentStreamView({
const getContent = () => collectTurnContent(index); const getContent = () => collectTurnContent(index);
return ( return (
<View style={[stylesheet.streamItemWrapper, { marginBottom: gapAbove }]}> <View style={[stylesheet.streamItemWrapper, { marginBottom: gapBelow }]}>
{content} {content}
{isEndOfTurn ? <TurnCopyButton getContent={getContent} /> : null} {isEndOfTurn ? <TurnCopyButton getContent={getContent} /> : null}
</View> </View>
); );
}, },
[ [
getGapAbove, getGapBelow,
renderStreamItemContent, renderStreamItemContent,
flatListData, flatListData,
agent.status, agent.status,