Description
On Android, rendered markdown tables that overflow horizontally are nearly impossible to scroll. Occasionally you can touch them in just the right spot and scroll, but it's extremely difficult and unreliable.
Root Cause
The table's horizontal ScrollView is nested inside three layers of gesture-consuming ancestors that compete for touch events on Android:
Layer 1: GestureDetector with LongPress (MarkdownView.tsx:84-99)
The entire markdown content (including the table) is wrapped in a GestureDetector for long-press-to-copy:
const longPressGesture = Gesture.LongPress()
.minDuration(500)
.onStart(() => { handleLongPress(); })
.runOnJS(true);
<GestureDetector gesture={longPressGesture}>
<View style={{ width: '100%' }}>
{renderContent()} // ← table's ScrollView is inside here
</View>
</GestureDetector>
The comment says "it doesn't block pan gestures so horizontal scrolling in code blocks and tables still works" — but on Android, react-native-gesture-handler's GestureDetector can still interfere with the touch event pipeline by consuming the initial touch-down, making it harder for the inner ScrollView to recognize a horizontal pan.
Layer 2: Pressable wrapper (MessageView.tsx:274)
Agent messages are wrapped in <Pressable> for hover detection:
<Pressable
style={styles.agentMessageContainer}
onHoverIn={isWeb ? () => setIsMessageHovered(true) : undefined}
onHoverOut={isWeb ? () => setIsMessageHovered(false) : undefined}
>
...
<MarkdownView markdown={markdown} ... />
...
</Pressable>
On Android, Pressable participates in the responder system and can delay or intercept touches intended for child scroll views.
Layer 3: Inverted FlatList (ChatList.tsx:429-477)
The parent chat list is an inverted FlatList:
<FlatList
inverted={true}
keyboardShouldPersistTaps="handled"
...
/>
The vertical FlatList handles vertical scroll gestures, and on Android it doesn't always correctly negotiate with nested horizontal ScrollViews — even when the inner ScrollView has nestedScrollEnabled={true}.
The table's ScrollView (MarkdownView.tsx:267-272)
<ScrollView
horizontal
showsHorizontalScrollIndicator={Platform.OS !== 'web'}
nestedScrollEnabled={true}
style={style.tableScrollView}
>
The ScrollView has nestedScrollEnabled={true}, which is the correct Android property for nested scrolling. But it's not sufficient when three ancestor gesture consumers compete for the same touch event.
Why it works "sometimes in just the right spot"
The gesture system uses hit-testing to determine which handler gets the touch:
- Touching text inside the table →
Pressable and GestureDetector both compete for the touch; the horizontal ScrollView loses
- Touching empty space (padding/borders) inside the table → the
ScrollView may win the gesture negotiation, allowing horizontal scroll
Suggested Fixes
-
Exclude tables from the GestureDetector wrapper — Render table blocks outside the GestureDetector, or use Gesture.Exclusive/Gesture.Simultaneous to explicitly allow pan gestures to pass through to table ScrollViews
-
Replace Pressable with a non-touch-consuming wrapper on mobile — The Pressable is only used for hover detection (onHoverIn/onHoverOut) which is web-only. On native, it could be a plain View that doesn't participate in the responder system
-
Use react-native-gesture-handler's ScrollView instead of RN's built-in ScrollView for the table — RNGH's ScrollView integrates better with the gesture handler system and may resolve the negotiation issue
-
Add simultaneousHandlers coordination — Configure the LongPress gesture to explicitly allow simultaneous pan gestures from the table's ScrollView
Affected Files
apps/ui/sources/components/markdown/MarkdownView.tsx (lines 84-99 gesture wrapper, lines 255-305 table rendering)
apps/ui/sources/components/sessions/transcript/MessageView.tsx (lines 274-319 Pressable wrapper)
apps/ui/sources/components/sessions/transcript/ChatList.tsx (lines 429-477 FlatList config)
🤖 Generated with Claude Code
Description
On Android, rendered markdown tables that overflow horizontally are nearly impossible to scroll. Occasionally you can touch them in just the right spot and scroll, but it's extremely difficult and unreliable.
Root Cause
The table's horizontal
ScrollViewis nested inside three layers of gesture-consuming ancestors that compete for touch events on Android:Layer 1:
GestureDetectorwithLongPress(MarkdownView.tsx:84-99)The entire markdown content (including the table) is wrapped in a
GestureDetectorfor long-press-to-copy:The comment says "it doesn't block pan gestures so horizontal scrolling in code blocks and tables still works" — but on Android,
react-native-gesture-handler'sGestureDetectorcan still interfere with the touch event pipeline by consuming the initial touch-down, making it harder for the innerScrollViewto recognize a horizontal pan.Layer 2:
Pressablewrapper (MessageView.tsx:274)Agent messages are wrapped in
<Pressable>for hover detection:On Android,
Pressableparticipates in the responder system and can delay or intercept touches intended for child scroll views.Layer 3: Inverted
FlatList(ChatList.tsx:429-477)The parent chat list is an inverted
FlatList:The vertical FlatList handles vertical scroll gestures, and on Android it doesn't always correctly negotiate with nested horizontal ScrollViews — even when the inner ScrollView has
nestedScrollEnabled={true}.The table's ScrollView (
MarkdownView.tsx:267-272)The ScrollView has
nestedScrollEnabled={true}, which is the correct Android property for nested scrolling. But it's not sufficient when three ancestor gesture consumers compete for the same touch event.Why it works "sometimes in just the right spot"
The gesture system uses hit-testing to determine which handler gets the touch:
PressableandGestureDetectorboth compete for the touch; the horizontalScrollViewlosesScrollViewmay win the gesture negotiation, allowing horizontal scrollSuggested Fixes
Exclude tables from the
GestureDetectorwrapper — Render table blocks outside theGestureDetector, or useGesture.Exclusive/Gesture.Simultaneousto explicitly allow pan gestures to pass through to table ScrollViewsReplace
Pressablewith a non-touch-consuming wrapper on mobile — ThePressableis only used for hover detection (onHoverIn/onHoverOut) which is web-only. On native, it could be a plainViewthat doesn't participate in the responder systemUse
react-native-gesture-handler'sScrollViewinstead of RN's built-inScrollViewfor the table — RNGH's ScrollView integrates better with the gesture handler system and may resolve the negotiation issueAdd
simultaneousHandlerscoordination — Configure theLongPressgesture to explicitly allow simultaneous pan gestures from the table's ScrollViewAffected Files
apps/ui/sources/components/markdown/MarkdownView.tsx(lines 84-99 gesture wrapper, lines 255-305 table rendering)apps/ui/sources/components/sessions/transcript/MessageView.tsx(lines 274-319 Pressable wrapper)apps/ui/sources/components/sessions/transcript/ChatList.tsx(lines 429-477 FlatList config)🤖 Generated with Claude Code