From 16c08878d26a93fde2e973791e1fb37874dd4014 Mon Sep 17 00:00:00 2001 From: Ugo <76181384+siure@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:30:03 +0000 Subject: [PATCH 1/3] fix(mobile): wrap Android source files when Word break is on The native canvas cannot wrap, so Word break still clipped long lines. Fall back to the JS renderer and put flex-1 on a wrapping view so Android does not clip Text. Co-authored-by: Cursor --- .../src/features/files/SourceFileSurface.tsx | 93 ++++++++++--------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/apps/mobile/src/features/files/SourceFileSurface.tsx b/apps/mobile/src/features/files/SourceFileSurface.tsx index 2eabce998e8e..42531c1b5a17 100644 --- a/apps/mobile/src/features/files/SourceFileSurface.tsx +++ b/apps/mobile/src/features/files/SourceFileSurface.tsx @@ -46,7 +46,7 @@ const HighlightedSourceLine = memo(function HighlightedSourceLine(props: { }) { return ( {props.index + 1} - - {props.tokens && props.tokens.length > 0 - ? (() => { - let offset = 0; - return props.tokens.map((token) => { - const start = offset; - offset += token.content.length; + + {props.tokens && props.tokens.length > 0 + ? (() => { + let offset = 0; + return props.tokens.map((token) => { + const start = offset; + offset += token.content.length; - const fontWeight = - token.fontStyle !== null && (token.fontStyle & 2) === 2 - ? ("700" as const) - : ("400" as const); - const fontStyle = - token.fontStyle !== null && (token.fontStyle & 1) === 1 - ? ("italic" as const) - : ("normal" as const); + const fontWeight = + token.fontStyle !== null && (token.fontStyle & 2) === 2 + ? ("700" as const) + : ("400" as const); + const fontStyle = + token.fontStyle !== null && (token.fontStyle & 1) === 1 + ? ("italic" as const) + : ("normal" as const); - return ( - - {token.content.length > 0 ? renderVisibleWhitespace(token.content) : " "} - - ); - }); - })() - : renderVisibleWhitespace(props.line || " ")} - + return ( + + {token.content.length > 0 ? renderVisibleWhitespace(token.content) : " "} + + ); + }); + })() + : renderVisibleWhitespace(props.line || " ")} + + ); }); @@ -282,7 +286,8 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { export function SourceFileSurface(props: SourceFileSurfaceProps) { const NativeView = resolveNativeReviewDiffView(); - return NativeView ? ( + const { codeWordBreak } = useAppearanceCodeSurface(); + return NativeView && !codeWordBreak ? ( ) : ( From 1bee10c9698d6b272eeba0050c3c1dd5e1515aa8 Mon Sep 17 00:00:00 2001 From: Ugo <76181384+siure@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:18:44 +0000 Subject: [PATCH 2/3] fix(mobile): keep pull-to-refresh on the JS source viewer Word break falls back to JavaScriptSourceFileSurface, which dropped onRefresh. Wire the same RefreshControl used by the native path. Co-authored-by: Cursor --- .../src/features/files/SourceFileSurface.tsx | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/features/files/SourceFileSurface.tsx b/apps/mobile/src/features/files/SourceFileSurface.tsx index 42531c1b5a17..23bc0e5fbf0b 100644 --- a/apps/mobile/src/features/files/SourceFileSurface.tsx +++ b/apps/mobile/src/features/files/SourceFileSurface.tsx @@ -2,7 +2,14 @@ import { useAtomValue } from "@effect/atom-react"; import { AsyncResult } from "effect/unstable/reactivity"; import type { ComponentType } from "react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { FlatList, ScrollView, Text as NativeText, useWindowDimensions, View } from "react-native"; +import { + FlatList, + RefreshControl, + ScrollView, + Text as NativeText, + useWindowDimensions, + View, +} from "react-native"; import { AppText as Text } from "../../components/AppText"; import { LoadingStrip } from "../../components/LoadingStrip"; @@ -216,9 +223,22 @@ function NativeSourceFileSurface( } function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { + const { onRefresh } = props; const { codeSurface, codeWordBreak } = useAppearanceCodeSurface(); const { lines, status, targetIndex, tokens } = useSourceFileModel(props); const listRef = useRef>(null); + const [isPullRefreshing, setIsPullRefreshing] = useState(false); + const handlePullToRefresh = useCallback(async () => { + if (!onRefresh) { + return; + } + setIsPullRefreshing(true); + try { + await onRefresh(); + } finally { + setIsPullRefreshing(false); + } + }, [onRefresh]); useEffect(() => { if (targetIndex === null) { @@ -267,6 +287,14 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { paddingTop: 8, }} renderItem={renderLine} + refreshControl={ + onRefresh ? ( + void handlePullToRefresh()} + /> + ) : undefined + } /> ); From 0b490ecff1fe5616f5e385a29497c0badbd328bc Mon Sep 17 00:00:00 2001 From: Ugo <76181384+siure@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:27:26 +0000 Subject: [PATCH 3/3] fix(mobile): retry line jumps when Word break skips item layout Wrapped lines cannot use getItemLayout, so scrollToIndex fails for targets outside the first window. Estimate an offset and retry. Co-authored-by: Cursor --- .../src/features/files/SourceFileSurface.tsx | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/features/files/SourceFileSurface.tsx b/apps/mobile/src/features/files/SourceFileSurface.tsx index 23bc0e5fbf0b..db37d7f477c9 100644 --- a/apps/mobile/src/features/files/SourceFileSurface.tsx +++ b/apps/mobile/src/features/files/SourceFileSurface.tsx @@ -227,6 +227,7 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { const { codeSurface, codeWordBreak } = useAppearanceCodeSurface(); const { lines, status, targetIndex, tokens } = useSourceFileModel(props); const listRef = useRef>(null); + const scrollRetryCountRef = useRef(0); const [isPullRefreshing, setIsPullRefreshing] = useState(false); const handlePullToRefresh = useCallback(async () => { if (!onRefresh) { @@ -240,15 +241,39 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { } }, [onRefresh]); + const scrollToLine = useCallback((index: number) => { + listRef.current?.scrollToIndex({ index, animated: false, viewPosition: 0.3 }); + }, []); + useEffect(() => { if (targetIndex === null) { return; } + scrollRetryCountRef.current = 0; const frame = requestAnimationFrame(() => { - listRef.current?.scrollToIndex({ index: targetIndex, animated: false, viewPosition: 0.3 }); + scrollToLine(targetIndex); }); return () => cancelAnimationFrame(frame); - }, [props.path, targetIndex]); + }, [props.path, scrollToLine, targetIndex]); + + const handleScrollToIndexFailed = useCallback( + (info: { index: number; averageItemLength: number }) => { + if (scrollRetryCountRef.current >= 5) { + return; + } + scrollRetryCountRef.current += 1; + const itemLength = + info.averageItemLength > 0 ? info.averageItemLength : codeSurface.rowHeight; + listRef.current?.scrollToOffset({ + offset: info.index * itemLength, + animated: false, + }); + requestAnimationFrame(() => { + scrollToLine(info.index); + }); + }, + [codeSurface.rowHeight, scrollToLine], + ); const renderLine = useCallback( ({ item, index }: { item: string; index: number }) => ( @@ -287,6 +312,7 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { paddingTop: 8, }} renderItem={renderLine} + onScrollToIndexFailed={handleScrollToIndexFailed} refreshControl={ onRefresh ? (