diff --git a/apps/mobile/src/components/AndroidScreenHeader.tsx b/apps/mobile/src/components/AndroidScreenHeader.tsx
index 46bc2c7c0912..f672830d3645 100644
--- a/apps/mobile/src/components/AndroidScreenHeader.tsx
+++ b/apps/mobile/src/components/AndroidScreenHeader.tsx
@@ -18,6 +18,7 @@ export function AndroidHeaderIconButton(props: {
readonly icon: AppSymbolName;
readonly onPress?: () => void;
readonly disabled?: boolean;
+ readonly filled?: boolean;
}) {
return (
diff --git a/apps/mobile/src/components/AppSymbol.tsx b/apps/mobile/src/components/AppSymbol.tsx
index aa714bccb419..1f6bbeec8135 100644
--- a/apps/mobile/src/components/AppSymbol.tsx
+++ b/apps/mobile/src/components/AppSymbol.tsx
@@ -81,6 +81,7 @@ import IconSun from "@tabler/icons-react-native/IconSun";
import IconTerminal2 from "@tabler/icons-react-native/IconTerminal2";
import IconTextDecrease from "@tabler/icons-react-native/IconTextDecrease";
import IconTextIncrease from "@tabler/icons-react-native/IconTextIncrease";
+import IconTextWrap from "@tabler/icons-react-native/IconTextWrap";
import IconTool from "@tabler/icons-react-native/IconTool";
import IconTrash from "@tabler/icons-react-native/IconTrash";
import IconTypography from "@tabler/icons-react-native/IconTypography";
@@ -96,6 +97,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial> = {
"arrow.branch": IconGitBranch,
"arrow.clockwise": IconRefresh,
"arrow.down.circle": IconArrowDownCircle,
+ "arrow.left.and.line.vertical.and.arrow.right": IconTextWrap,
"arrow.right.circle": IconArrowRightCircle,
"arrow.triangle.branch": IconGitBranch,
"arrow.triangle.pull": IconGitPullRequest,
diff --git a/apps/mobile/src/features/files/SourceFileSurface.tsx b/apps/mobile/src/features/files/SourceFileSurface.tsx
index 2eabce998e8e..b308f550eeb4 100644
--- a/apps/mobile/src/features/files/SourceFileSurface.tsx
+++ b/apps/mobile/src/features/files/SourceFileSurface.tsx
@@ -2,7 +2,7 @@ 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, ScrollView, Text as NativeText, View } from "react-native";
import { AppText as Text } from "../../components/AppText";
import { LoadingStrip } from "../../components/LoadingStrip";
@@ -50,6 +50,7 @@ const HighlightedSourceLine = memo(function HighlightedSourceLine(props: {
style={{ minHeight: props.codeSurface.rowHeight }}
>
{
@@ -179,9 +180,7 @@ function NativeSourceFileSurface(
[appTheme, themeAppearance, themeId],
);
const styleJson = useMemo(() => JSON.stringify(nativeSourceStyle), [nativeSourceStyle]);
- const contentWidth = codeWordBreak
- ? Math.max(240, viewportWidth - codeSurface.gutterWidth - 24)
- : NATIVE_SOURCE_CONTENT_WIDTH;
+ const contentWidth = NATIVE_SOURCE_CONTENT_WIDTH;
return (
@@ -215,15 +214,39 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) {
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 (!props.onRefresh) {
+ return;
+ }
+ setIsPullRefreshing(true);
+ try {
+ await props.onRefresh();
+ } finally {
+ setIsPullRefreshing(false);
+ }
+ }, [props.onRefresh]);
+ const scrollRetryCountRef = useRef(0);
+ const retryFrameRef = useRef(null);
+ const targetIndexRef = useRef(null);
+ targetIndexRef.current = targetIndex;
useEffect(() => {
if (targetIndex === null) {
return;
}
+ scrollRetryCountRef.current = 0;
const frame = requestAnimationFrame(() => {
listRef.current?.scrollToIndex({ index: targetIndex, animated: false, viewPosition: 0.3 });
});
- return () => cancelAnimationFrame(frame);
+ return () => {
+ cancelAnimationFrame(frame);
+ if (retryFrameRef.current !== null) {
+ cancelAnimationFrame(retryFrameRef.current);
+ retryFrameRef.current = null;
+ }
+ };
}, [props.path, targetIndex]);
const renderLine = useCallback(
@@ -240,6 +263,31 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) {
[codeSurface, codeWordBreak, targetIndex, tokens],
);
+ const handleScrollToIndexFailed = useCallback(
+ (info: { index: number; averageItemLength: number }) => {
+ listRef.current?.scrollToOffset({
+ offset: Math.max(0, info.averageItemLength * info.index),
+ animated: false,
+ });
+ if (retryFrameRef.current !== null || scrollRetryCountRef.current >= 3) {
+ return;
+ }
+ scrollRetryCountRef.current += 1;
+ retryFrameRef.current = requestAnimationFrame(() => {
+ retryFrameRef.current = null;
+ if (targetIndexRef.current === null || targetIndexRef.current !== info.index) {
+ return;
+ }
+ listRef.current?.scrollToIndex({
+ index: info.index,
+ animated: false,
+ viewPosition: 0.3,
+ });
+ });
+ },
+ [],
+ );
+
const list = (
void handlePullToRefresh(),
+ }
+ : {})}
/>
);
@@ -281,6 +336,10 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) {
}
export function SourceFileSurface(props: SourceFileSurfaceProps) {
+ const { codeWordBreak } = useAppearanceCodeSurface();
+ if (codeWordBreak) {
+ return ;
+ }
const NativeView = resolveNativeReviewDiffView();
return NativeView ? (
diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
index 29bbdb49c890..ee04057afba5 100644
--- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
+++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
@@ -547,6 +547,9 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
readonly mode: FileViewMode;
} | null>(null);
const [previewRevision, setPreviewRevision] = useState(0);
+ const { appearance, setCodeWordBreak } = useAppearancePreferences();
+ const codeWordBreak = appearance.codeWordBreak;
+ const primaryColor = useUniwindTheme()["--color-primary"];
const previewKey = JSON.stringify([environmentId, cwd, relativePath, previewRevision]);
const [fullScreenPreview, setFullScreenPreview] = useState(null);
const isVideoFile = relativePath !== null && isVideoPreviewFile(relativePath);
@@ -562,6 +565,9 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
? modeOverride.mode
: defaultViewMode(relativePath);
const resolvedActiveMode = isVideoFile ? "preview" : canPreview ? activeMode : "source";
+ const handleToggleWordBreak = useCallback(() => {
+ setCodeWordBreak(!codeWordBreak);
+ }, [codeWordBreak, setCodeWordBreak]);
const assetPreviewPath = isBrowserFile || isImageFile || isVideoFile ? relativePath : null;
const assetPreview = useWorkspaceFileAssetUrlState({
cwd,
@@ -843,6 +849,14 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
onBack={handleBack}
trailing={
<>
+ {resolvedActiveMode === "source" ? (
+
+ ) : null}
{fileInspector.supported ? (
) : null}
+ {resolvedActiveMode === "source" ? (
+
+ ) : null}
{fileMenuActions.some(({ inline }) => inline) ? (