Fix iOS Sources panel expansion in agent citations (#9567)

* Fix iOS citations panel toggle reliability

Remove the animated maxHeight/opacity accordion from agent citations so the Sources section expands and collapses consistently on iOS without hidden or underlapping content.

Made-with: Cursor

* Restore citations panel animation with stable transitions

Use Reanimated enter/exit and layout transitions for the Sources panel so opening and closing stay animated while preserving the iOS reliability fix.

Made-with: Cursor

* Fix citations panel collapse animation overlapping content below

Replace entering/exiting animations with shared-value-driven height
and opacity transitions. Reanimated's exiting animations pull the view
out of layout flow as an absolute overlay, causing the fading content
to overlap the regenerate button during collapse. Using an always-mounted
Animated.View with measured content height and overflow clipping ensures
smooth expand/collapse without overlap.

Made-with: Cursor
This commit is contained in:
Nick Misasi 2026-03-03 11:03:31 -05:00 committed by GitHub
parent c391484cc8
commit e977303275
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -2,8 +2,8 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {TOUCH_TARGET_SIZE} from '@agents/constants'; import {TOUCH_TARGET_SIZE} from '@agents/constants';
import React, {useCallback, useState} from 'react'; import React, {useCallback, useEffect, useState} from 'react';
import {Text, TouchableOpacity, View} from 'react-native'; import {type LayoutChangeEvent, Text, TouchableOpacity, View} from 'react-native';
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import CompassIcon from '@components/compass_icon'; import CompassIcon from '@components/compass_icon';
@ -41,7 +41,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
marginLeft: 8, marginLeft: 8,
}, },
citationsList: { citationsList: {
marginTop: 4, overflow: 'hidden',
},
citationsContentWrapper: {
position: 'absolute',
width: '100%',
}, },
citationItem: { citationItem: {
flexDirection: 'row', flexDirection: 'row',
@ -90,15 +94,22 @@ const CitationsList = ({annotations}: CitationsListProps) => {
const theme = useTheme(); const theme = useTheme();
const styles = getStyleSheet(theme); const styles = getStyleSheet(theme);
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const animatedHeight = useSharedValue(0); const progress = useSharedValue(0);
const opacity = useSharedValue(0); const contentHeight = useSharedValue(0);
useEffect(() => {
progress.value = withTiming(isExpanded ? 1 : 0, {duration: 250});
// eslint-disable-next-line react-hooks/exhaustive-deps -- progress is a stable shared value ref
}, [isExpanded]);
const handleToggle = useCallback(() => { const handleToggle = useCallback(() => {
const newExpanded = !isExpanded; setIsExpanded((prev) => !prev);
setIsExpanded(newExpanded); }, []);
animatedHeight.value = withTiming(newExpanded ? 1 : 0, {duration: 250});
opacity.value = withTiming(newExpanded ? 1 : 0, {duration: 250}); const handleContentLayout = useCallback((e: LayoutChangeEvent) => {
}, [isExpanded, animatedHeight, opacity]); contentHeight.value = e.nativeEvent.layout.height;
// eslint-disable-next-line react-hooks/exhaustive-deps -- contentHeight is a stable shared value ref
}, []);
const handleCitationPress = useCallback((url: string) => { const handleCitationPress = useCallback((url: string) => {
if (url) { if (url) {
@ -106,11 +117,14 @@ const CitationsList = ({annotations}: CitationsListProps) => {
} }
}, []); }, []);
const listAnimatedStyle = useAnimatedStyle(() => ({ const collapsibleStyle = useAnimatedStyle(() => {
opacity: opacity.value, const p = progress.value;
maxHeight: animatedHeight.value === 0 ? 0 : undefined, return {
overflow: 'hidden', height: contentHeight.value > 0 ? p * contentHeight.value : 0,
})); opacity: p,
marginTop: p * 4,
};
});
return ( return (
<View style={styles.container}> <View style={styles.container}>
@ -139,11 +153,14 @@ const CitationsList = ({annotations}: CitationsListProps) => {
/> />
</TouchableOpacity> </TouchableOpacity>
{isExpanded && ( <Animated.View style={[styles.citationsList, collapsibleStyle]}>
<Animated.View style={[styles.citationsList, listAnimatedStyle]}> <View
{annotations.map((annotation, idx) => ( onLayout={handleContentLayout}
style={styles.citationsContentWrapper}
>
{annotations.map((annotation) => (
<TouchableOpacity <TouchableOpacity
key={`citation-${annotation.index}-${idx}`} key={`citation-${annotation.index}-${annotation.url}`}
onPress={() => handleCitationPress(annotation.url)} onPress={() => handleCitationPress(annotation.url)}
style={styles.citationItem} style={styles.citationItem}
testID={`citations.list.item.${annotation.index}`} testID={`citations.list.item.${annotation.index}`}
@ -176,8 +193,8 @@ const CitationsList = ({annotations}: CitationsListProps) => {
/> />
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View>
</Animated.View> </Animated.View>
)}
</View> </View>
); );
}; };