import type { EnvironmentId, ThreadId } from "@t3tools/contracts";import { useLocalSearchParams } from "expo-router";import Stack from "expo-router/stack";import { SymbolView } from "expo-symbols";import { memo, type ReactElement, useCallback, useMemo } from "react";import { ActivityIndicator, Pressable, ScrollView, type NativeSyntheticEvent, Text as NativeText, StyleSheet, useColorScheme, View,} from "react-native";import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppText as Text } from "../../components/AppText";import { environmentCatalog } from "../../connection/catalog";import { useEnvironmentPresentation } from "../../state/presentation";import { useAtomCommand } from "../../state/use-atom-command";import { useThemeColor } from "../../lib/useThemeColor";import { MOBILE_TYPOGRAPHY } from "../../lib/typography";import { useThreadDraftForThread } from "../../state/use-thread-composer-state";import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice";import { useReviewCacheForThread } from "./reviewState";import { resolveNativeReviewDiffView } from "../diffs/nativeReviewDiffSurface";import { NATIVE_REVIEW_DIFF_CONTENT_WIDTH, NATIVE_REVIEW_DIFF_ROW_HEIGHT,} from "./nativeReviewDiffAdapter";import { useReviewDiffData } from "./useReviewDiffData";import { useReviewFileVisibility } from "./reviewFileVisibility";import { useReviewSections } from "./useReviewSections";import { useNativeReviewDiffBridge } from "./useNativeReviewDiffBridge";import { useReviewCommentSelectionController } from "./useReviewCommentSelectionController";import { resolveReviewAvailability } from "./reviewAvailability"; const IOS_NAV_BAR_HEIGHT = 44;const REVIEW_HEADER_SPACING = 0; const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) { return ( <View className="border-b border-amber-200 bg-amber-50 px-4 py-3 dark:border-amber-900/60 dark:bg-amber-950/40"> <Text className="text-xs font-t3-bold uppercase text-amber-700 dark:text-amber-300"> Partial diff </Text> <Text className="text-xs leading-[18px] text-amber-800 dark:text-amber-200"> {props.notice} </Text> </View> );}); function ReviewSelectionActionBar(props: { readonly bottomInset: number; readonly title: string | null; readonly onOpenComment: (() => void) | null; readonly onClear: () => void;}) { if (!props.title) { return null; } const content = ( <> <SymbolView name={props.onOpenComment ? "text.bubble" : "line.3.horizontal.decrease.circle"} size={16} tintColor="#ffffff" type="monochrome" /> <Text className="text-base font-t3-bold text-white">{props.title}</Text> </> ); return ( <View pointerEvents="box-none" style={{ position: "absolute", left: 18, right: 18, bottom: Math.max(props.bottomInset, 10) + 18, flexDirection: "row", justifyContent: "center", gap: 10, }} > {props.onOpenComment ? ( <Pressable className="h-12 flex-1 flex-row items-center justify-center gap-2 rounded-full bg-blue-600 px-5" onPress={props.onOpenComment} > {content} </Pressable> ) : ( <View className="h-12 flex-1 flex-row items-center justify-center gap-2 rounded-full bg-blue-600 px-5"> {content} </View> )} <Pressable className="h-12 w-12 items-center justify-center rounded-full bg-blue-600" onPress={props.onClear} > <SymbolView name="xmark" size={16} tintColor="#ffffff" type="monochrome" /> </Pressable> </View> );} export function ReviewSheet() { const insets = useSafeAreaInsets(); const colorScheme = useColorScheme(); const headerForeground = String(useThemeColor("--color-foreground")); const headerMuted = String(useThemeColor("--color-foreground-muted")); const headerIcon = String(useThemeColor("--color-icon")); const { environmentId, threadId } = useLocalSearchParams<{ environmentId: EnvironmentId; threadId: ThreadId; }>(); const environment = useEnvironmentPresentation(environmentId); const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry"); const isEnvironmentReady = environment.presentation?.connection.phase === "connected"; const { draftMessage } = useThreadDraftForThread({ environmentId, threadId }); const reviewCache = useReviewCacheForThread({ environmentId, threadId }); const selectedTheme = colorScheme === "dark" ? "dark" : "light"; const topContentInset = insets.top + IOS_NAV_BAR_HEIGHT; const { error, loadingGitDiffs, loadingTurnIds, reviewSections, selectedSection, refreshSelectedSection, selectSection, } = useReviewSections({ enabled: isEnvironmentReady, environmentId, threadId, reviewCache, }); const { headerDiffSummary, nativeReviewDiffData, parsedDiff, pendingReviewCommentCount } = useReviewDiffData({ threadKey: reviewCache.threadKey, selectedSection, draftMessage, }); const NativeReviewDiffView = resolveNativeReviewDiffView()!; const reviewFiles = parsedDiff.kind === "files" ? parsedDiff.files : []; const fileVisibility = useReviewFileVisibility({ threadKey: reviewCache.threadKey, sectionId: selectedSection?.id ?? null, files: reviewFiles, cachedExpandedFileIds: selectedSection?.id ? reviewCache.expandedFileIdsBySection[selectedSection.id] : undefined, cachedViewedFileIds: selectedSection?.id ? reviewCache.viewedFileIdsBySection[selectedSection.id] : undefined, }); const { collapsedFileIds, toggleExpandedFile, toggleViewedFile, viewedFileIds } = fileVisibility; const commentSelection = useReviewCommentSelectionController({ environmentId, threadId, selectedSection, nativeReviewDiffData, }); const nativeBridge = useNativeReviewDiffBridge({ threadKey: reviewCache.threadKey, sectionId: selectedSection?.id ?? null, diff: selectedSection?.diff, data: nativeReviewDiffData, scheme: selectedTheme, collapsedFileIds, viewedFileIds, selectedRowIds: commentSelection.selectedRowIds, canHighlight: parsedDiff.kind === "files", }); const handleNativeToggleFile = useCallback( (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => { const { fileId } = event.nativeEvent; if (fileId) { toggleExpandedFile(fileId); } }, [toggleExpandedFile], ); const handleNativeToggleViewedFile = useCallback( (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => { const { fileId } = event.nativeEvent; if (fileId) { toggleViewedFile(fileId); } }, [toggleViewedFile], ); const parsedDiffNotice = parsedDiff.kind === "files" || parsedDiff.kind === "raw" ? parsedDiff.notice : null; const hasCachedSelectedDiff = selectedSection?.diff != null; const hasAnyCachedDiff = reviewSections.some((section) => section.diff != null); const { showConnectionNotice, showSectionToolbar } = resolveReviewAvailability({ hasEnvironmentPresentation: environment.isReady, isEnvironmentConnected: isEnvironmentReady, hasCachedSelectedDiff, hasAnyCachedDiff, }); const handleRetryEnvironment = useCallback(() => { void retryEnvironment(environmentId); }, [environmentId, retryEnvironment]); const listHeader = useMemo(() => { const children: ReactElement[] = []; if (error) { children.push( <View key="review-error" className="border-b border-border bg-card px-4 py-3"> <Text className="text-sm font-t3-bold text-foreground">Review unavailable</Text> <Text className="text-xs leading-[18px] text-foreground-muted">{error}</Text> </View>, ); } if (parsedDiffNotice) { children.push(<ReviewNotice key="review-notice" notice={parsedDiffNotice} />); } if (children.length === 0) { return null; } return <>{children}</>; }, [error, parsedDiffNotice]); return ( <> <Stack.Screen options={{ headerTransparent: true, headerShadowVisible: false, headerTintColor: headerIcon, headerStyle: { backgroundColor: "transparent", }, headerTitle: () => ( <View style={{ alignItems: "center" }}> <NativeText numberOfLines={1} style={{ fontFamily: "DMSans_700Bold", fontSize: MOBILE_TYPOGRAPHY.headline.fontSize, fontWeight: "900", color: headerForeground, letterSpacing: -0.4, }} > Files Changed </NativeText> <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 6, flexWrap: "wrap", }} > {headerDiffSummary.additions && headerDiffSummary.deletions ? ( <> <NativeText style={{ fontFamily: "DMSans_700Bold", fontSize: MOBILE_TYPOGRAPHY.label.fontSize, fontWeight: "700", color: "#16a34a", }} > {headerDiffSummary.additions} </NativeText> <NativeText style={{ fontFamily: "DMSans_700Bold", fontSize: MOBILE_TYPOGRAPHY.label.fontSize, fontWeight: "700", color: "#e11d48", }} > {headerDiffSummary.deletions} </NativeText> {pendingReviewCommentCount > 0 ? ( <NativeText style={{ fontFamily: "DMSans_700Bold", fontSize: MOBILE_TYPOGRAPHY.label.fontSize, fontWeight: "700", color: "#b45309", }} > {pendingReviewCommentCount} pending </NativeText> ) : null} </> ) : ( <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}> <NativeText numberOfLines={1} style={{ fontFamily: "DMSans_700Bold", fontSize: MOBILE_TYPOGRAPHY.label.fontSize, fontWeight: "700", color: headerMuted, }} > {selectedSection?.title ?? "Review changes"} </NativeText> {pendingReviewCommentCount > 0 ? ( <NativeText style={{ fontFamily: "DMSans_700Bold", fontSize: MOBILE_TYPOGRAPHY.label.fontSize, fontWeight: "700", color: "#b45309", }} > {pendingReviewCommentCount} pending </NativeText> ) : null} </View> )} </View> </View> ), }} /> {showSectionToolbar ? ( <Stack.Toolbar placement="right"> <Stack.Toolbar.Menu icon="ellipsis.circle" title="Select diff" separateBackground> {reviewSections.map((section) => ( <Stack.Toolbar.MenuAction key={section.id} icon={section.id === selectedSection?.id ? "checkmark" : "circle"} onPress={() => selectSection(section.id)} subtitle={section.subtitle ?? undefined} > <Stack.Toolbar.Label>{section.title}</Stack.Toolbar.Label> </Stack.Toolbar.MenuAction> ))} <Stack.Toolbar.MenuAction icon="arrow.clockwise" disabled={ loadingGitDiffs || (selectedSection?.kind === "turn" && loadingTurnIds[selectedSection.id] === true) } onPress={() => void refreshSelectedSection()} subtitle="Reload current diff" > <Stack.Toolbar.Label>Refresh</Stack.Toolbar.Label> </Stack.Toolbar.MenuAction> </Stack.Toolbar.Menu> </Stack.Toolbar> ) : null} <View className="flex-1 bg-sheet"> {showConnectionNotice ? ( <View style={{ flex: 1, paddingTop: topContentInset }}> <EnvironmentConnectionNotice environmentLabel={environment.presentation?.entry.target.label ?? "Environment"} connection={ environment.presentation?.connection ?? { phase: "available", error: null, traceId: null, } } resourceName="review" onRetry={handleRetryEnvironment} /> </View> ) : selectedSection && parsedDiff.kind === "files" ? ( <View className="flex-1" style={{ backgroundColor: nativeBridge.theme.background, paddingTop: topContentInset + REVIEW_HEADER_SPACING, }} > {listHeader} <View className="flex-1" collapsable={false}> <NativeReviewDiffView key={`${reviewCache.threadKey}:${selectedSection.id}`} collapsable={false} testID="review-native-diff-view" style={StyleSheet.absoluteFill} appearanceScheme={selectedTheme} collapsedFileIdsJson={nativeBridge.collapsedFileIdsJson} collapsedCommentIdsJson={nativeBridge.collapsedCommentIdsJson} contentWidth={NATIVE_REVIEW_DIFF_CONTENT_WIDTH} rowHeight={NATIVE_REVIEW_DIFF_ROW_HEIGHT} rowsJson={nativeBridge.rowsJson} selectedRowIdsJson={nativeBridge.selectedRowIdsJson} styleJson={nativeBridge.styleJson} themeJson={nativeBridge.themeJson} tokensPatchJson={nativeBridge.tokensPatchJson} tokensResetKey={nativeBridge.tokensResetKey} viewedFileIdsJson={nativeBridge.viewedFileIdsJson} onDebug={nativeBridge.onDebug} onPressLine={commentSelection.onPressLine} onToggleComment={nativeBridge.onToggleComment} onToggleFile={handleNativeToggleFile} onToggleViewedFile={handleNativeToggleViewedFile} /> </View> </View> ) : ( <ScrollView contentInsetAdjustmentBehavior="never" contentInset={{ top: topContentInset, bottom: Math.max(insets.bottom, 18) + 18 }} contentOffset={{ x: 0, y: -topContentInset }} scrollIndicatorInsets={{ top: topContentInset, bottom: Math.max(insets.bottom, 18) + 18, }} showsVerticalScrollIndicator={false} style={{ flex: 1 }} > {listHeader} {!selectedSection ? ( <View className="border-b border-border bg-card px-4 py-5"> <Text className="text-sm font-t3-bold text-foreground">No review diffs</Text> <Text className="text-xs leading-[18px] text-foreground-muted"> This thread has no ready turn diffs and the worktree diff is empty. </Text> </View> ) : selectedSection.isLoading && selectedSection.diff === null ? ( <View className="items-center gap-3 border-b border-border bg-card px-4 py-6"> <ActivityIndicator size="small" /> <Text className="text-xs text-foreground-muted">Loading diff…</Text> </View> ) : parsedDiff.kind === "empty" ? ( <View className="border-b border-border bg-card px-4 py-5"> <Text className="text-sm font-t3-bold text-foreground">No changes</Text> <Text className="text-xs leading-[18px] text-foreground-muted"> {selectedSection.subtitle ?? "This diff is empty."} </Text> </View> ) : parsedDiff.kind === "raw" ? ( <View className="gap-3 border-b border-border bg-card px-4 py-4"> <Text className="text-xs leading-[18px] text-foreground-muted"> {parsedDiff.reason} </Text> <ScrollView horizontal showsHorizontalScrollIndicator={false} bounces={false}> <Text selectable className="font-mono text-xs leading-[19px] text-foreground"> {parsedDiff.text} </Text> </ScrollView> </View> ) : null} </ScrollView> )} <ReviewSelectionActionBar bottomInset={insets.bottom} title={commentSelection.selectionAction?.title ?? null} onOpenComment={commentSelection.selectionAction?.onOpenComment ?? null} onClear={commentSelection.clearSelection} /> </View> </> );}
import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
import { useLocalSearchParams } from "expo-router";
import Stack from "expo-router/stack";
import { SymbolView } from "expo-symbols";
import { memo, type ReactElement, useCallback, useMemo } from "react";
import {
ActivityIndicator,
Pressable,
ScrollView,
type NativeSyntheticEvent,
Text as NativeText,
StyleSheet,
useColorScheme,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppText as Text } from "../../components/AppText";
import { environmentCatalog } from "../../connection/catalog";
import { useEnvironmentPresentation } from "../../state/presentation";
import { useAtomCommand } from "../../state/use-atom-command";
import { useThemeColor } from "../../lib/useThemeColor";
import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
import { useThreadDraftForThread } from "../../state/use-thread-composer-state";
import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice";
import { useReviewCacheForThread } from "./reviewState";
import { resolveNativeReviewDiffView } from "../diffs/nativeReviewDiffSurface";
import {
NATIVE_REVIEW_DIFF_CONTENT_WIDTH,
NATIVE_REVIEW_DIFF_ROW_HEIGHT,
} from "./nativeReviewDiffAdapter";
import { useReviewDiffData } from "./useReviewDiffData";
import { useReviewFileVisibility } from "./reviewFileVisibility";
import { useReviewSections } from "./useReviewSections";
import { useNativeReviewDiffBridge } from "./useNativeReviewDiffBridge";
import { useReviewCommentSelectionController } from "./useReviewCommentSelectionController";
import { resolveReviewAvailability } from "./reviewAvailability";
const IOS_NAV_BAR_HEIGHT = 44;
const REVIEW_HEADER_SPACING = 0;
const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) {
return (
<View className="border-b border-amber-200 bg-amber-50 px-4 py-3 dark:border-amber-900/60 dark:bg-amber-950/40">
<Text className="text-xs font-t3-bold uppercase text-amber-700 dark:text-amber-300">
Partial diff
</Text>
<Text className="text-xs leading-[18px] text-amber-800 dark:text-amber-200">
{props.notice}
</Text>
</View>
);
});
function ReviewSelectionActionBar(props: {
readonly bottomInset: number;
readonly title: string | null;
readonly onOpenComment: (() => void) | null;
readonly onClear: () => void;
}) {
if (!props.title) {
return null;
}
const content = (
<>
<SymbolView
name={props.onOpenComment ? "text.bubble" : "line.3.horizontal.decrease.circle"}
size={16}
tintColor="#ffffff"
type="monochrome"
/>
<Text className="text-base font-t3-bold text-white">{props.title}</Text>
</>
);
return (
<View
pointerEvents="box-none"
style={{
position: "absolute",
left: 18,
right: 18,
bottom: Math.max(props.bottomInset, 10) + 18,
flexDirection: "row",
justifyContent: "center",
gap: 10,
}}
>
{props.onOpenComment ? (
<Pressable
className="h-12 flex-1 flex-row items-center justify-center gap-2 rounded-full bg-blue-600 px-5"
onPress={props.onOpenComment}
>
{content}
</Pressable>
) : (
<View className="h-12 flex-1 flex-row items-center justify-center gap-2 rounded-full bg-blue-600 px-5">
{content}
</View>
)}
<Pressable
className="h-12 w-12 items-center justify-center rounded-full bg-blue-600"
onPress={props.onClear}
>
<SymbolView name="xmark" size={16} tintColor="#ffffff" type="monochrome" />
</Pressable>
</View>
);
}
export function ReviewSheet() {
const insets = useSafeAreaInsets();
const colorScheme = useColorScheme();
const headerForeground = String(useThemeColor("--color-foreground"));
const headerMuted = String(useThemeColor("--color-foreground-muted"));
const headerIcon = String(useThemeColor("--color-icon"));
const { environmentId, threadId } = useLocalSearchParams<{
environmentId: EnvironmentId;
threadId: ThreadId;
}>();
const environment = useEnvironmentPresentation(environmentId);
const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry");
const isEnvironmentReady = environment.presentation?.connection.phase === "connected";
const { draftMessage } = useThreadDraftForThread({ environmentId, threadId });
const reviewCache = useReviewCacheForThread({ environmentId, threadId });
const selectedTheme = colorScheme === "dark" ? "dark" : "light";
const topContentInset = insets.top + IOS_NAV_BAR_HEIGHT;
const {
error,
loadingGitDiffs,
loadingTurnIds,
reviewSections,
selectedSection,
refreshSelectedSection,
selectSection,
} = useReviewSections({
enabled: isEnvironmentReady,
environmentId,
threadId,
reviewCache,
});
const { headerDiffSummary, nativeReviewDiffData, parsedDiff, pendingReviewCommentCount } =
useReviewDiffData({
threadKey: reviewCache.threadKey,
selectedSection,
draftMessage,
});
const NativeReviewDiffView = resolveNativeReviewDiffView()!;
const reviewFiles = parsedDiff.kind === "files" ? parsedDiff.files : [];
const fileVisibility = useReviewFileVisibility({
threadKey: reviewCache.threadKey,
sectionId: selectedSection?.id ?? null,
files: reviewFiles,
cachedExpandedFileIds: selectedSection?.id
? reviewCache.expandedFileIdsBySection[selectedSection.id]
: undefined,
cachedViewedFileIds: selectedSection?.id
? reviewCache.viewedFileIdsBySection[selectedSection.id]
: undefined,
});
const { collapsedFileIds, toggleExpandedFile, toggleViewedFile, viewedFileIds } = fileVisibility;
const commentSelection = useReviewCommentSelectionController({
environmentId,
threadId,
selectedSection,
nativeReviewDiffData,
});
const nativeBridge = useNativeReviewDiffBridge({
threadKey: reviewCache.threadKey,
sectionId: selectedSection?.id ?? null,
diff: selectedSection?.diff,
data: nativeReviewDiffData,
scheme: selectedTheme,
collapsedFileIds,
viewedFileIds,
selectedRowIds: commentSelection.selectedRowIds,
canHighlight: parsedDiff.kind === "files",
});
const handleNativeToggleFile = useCallback(
(event: NativeSyntheticEvent<{ readonly fileId?: string }>) => {
const { fileId } = event.nativeEvent;
if (fileId) {
toggleExpandedFile(fileId);
}
},
[toggleExpandedFile],
);
const handleNativeToggleViewedFile = useCallback(
(event: NativeSyntheticEvent<{ readonly fileId?: string }>) => {
const { fileId } = event.nativeEvent;
if (fileId) {
toggleViewedFile(fileId);
}
},
[toggleViewedFile],
);
const parsedDiffNotice =
parsedDiff.kind === "files" || parsedDiff.kind === "raw" ? parsedDiff.notice : null;
const hasCachedSelectedDiff = selectedSection?.diff != null;
const hasAnyCachedDiff = reviewSections.some((section) => section.diff != null);
const { showConnectionNotice, showSectionToolbar } = resolveReviewAvailability({
hasEnvironmentPresentation: environment.isReady,
isEnvironmentConnected: isEnvironmentReady,
hasCachedSelectedDiff,
hasAnyCachedDiff,
});
const handleRetryEnvironment = useCallback(() => {
void retryEnvironment(environmentId);
}, [environmentId, retryEnvironment]);
const listHeader = useMemo(() => {
const children: ReactElement[] = [];
if (error) {
children.push(
<View key="review-error" className="border-b border-border bg-card px-4 py-3">
<Text className="text-sm font-t3-bold text-foreground">Review unavailable</Text>
<Text className="text-xs leading-[18px] text-foreground-muted">{error}</Text>
</View>,
);
}
if (parsedDiffNotice) {
children.push(<ReviewNotice key="review-notice" notice={parsedDiffNotice} />);
}
if (children.length === 0) {
return null;
}
return <>{children}</>;
}, [error, parsedDiffNotice]);
return (
<>
<Stack.Screen
options={{
headerTransparent: true,
headerShadowVisible: false,
headerTintColor: headerIcon,
headerStyle: {
backgroundColor: "transparent",
},
headerTitle: () => (
<View style={{ alignItems: "center" }}>
<NativeText
numberOfLines={1}
style={{
fontFamily: "DMSans_700Bold",
fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
fontWeight: "900",
color: headerForeground,
letterSpacing: -0.4,
}}
>
Files Changed
</NativeText>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 6,
flexWrap: "wrap",
}}
>
{headerDiffSummary.additions && headerDiffSummary.deletions ? (
<>
<NativeText
style={{
fontFamily: "DMSans_700Bold",
fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
fontWeight: "700",
color: "#16a34a",
}}
>
{headerDiffSummary.additions}
</NativeText>
<NativeText
style={{
fontFamily: "DMSans_700Bold",
fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
fontWeight: "700",
color: "#e11d48",
}}
>
{headerDiffSummary.deletions}
</NativeText>
{pendingReviewCommentCount > 0 ? (
<NativeText
style={{
fontFamily: "DMSans_700Bold",
fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
fontWeight: "700",
color: "#b45309",
}}
>
{pendingReviewCommentCount} pending
</NativeText>
) : null}
</>
) : (
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
<NativeText
numberOfLines={1}
style={{
fontFamily: "DMSans_700Bold",
fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
fontWeight: "700",
color: headerMuted,
}}
>
{selectedSection?.title ?? "Review changes"}
</NativeText>
{pendingReviewCommentCount > 0 ? (
<NativeText
style={{
fontFamily: "DMSans_700Bold",
fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
fontWeight: "700",
color: "#b45309",
}}
>
{pendingReviewCommentCount} pending
</NativeText>
) : null}
</View>
)}
</View>
</View>
),
}}
/>
{showSectionToolbar ? (
<Stack.Toolbar placement="right">
<Stack.Toolbar.Menu icon="ellipsis.circle" title="Select diff" separateBackground>
{reviewSections.map((section) => (
<Stack.Toolbar.MenuAction
key={section.id}
icon={section.id === selectedSection?.id ? "checkmark" : "circle"}
onPress={() => selectSection(section.id)}
subtitle={section.subtitle ?? undefined}
>
<Stack.Toolbar.Label>{section.title}</Stack.Toolbar.Label>
</Stack.Toolbar.MenuAction>
))}
<Stack.Toolbar.MenuAction
icon="arrow.clockwise"
disabled={
loadingGitDiffs ||
(selectedSection?.kind === "turn" && loadingTurnIds[selectedSection.id] === true)
}
onPress={() => void refreshSelectedSection()}
subtitle="Reload current diff"
>
<Stack.Toolbar.Label>Refresh</Stack.Toolbar.Label>
</Stack.Toolbar.MenuAction>
</Stack.Toolbar.Menu>
</Stack.Toolbar>
) : null}
<View className="flex-1 bg-sheet">
{showConnectionNotice ? (
<View style={{ flex: 1, paddingTop: topContentInset }}>
<EnvironmentConnectionNotice
environmentLabel={environment.presentation?.entry.target.label ?? "Environment"}
connection={
environment.presentation?.connection ?? {
phase: "available",
error: null,
traceId: null,
}
}
resourceName="review"
onRetry={handleRetryEnvironment}
/>
</View>
) : selectedSection && parsedDiff.kind === "files" ? (
<View
className="flex-1"
style={{
backgroundColor: nativeBridge.theme.background,
paddingTop: topContentInset + REVIEW_HEADER_SPACING,
}}
>
{listHeader}
<View className="flex-1" collapsable={false}>
<NativeReviewDiffView
key={`${reviewCache.threadKey}:${selectedSection.id}`}
collapsable={false}
testID="review-native-diff-view"
style={StyleSheet.absoluteFill}
appearanceScheme={selectedTheme}
collapsedFileIdsJson={nativeBridge.collapsedFileIdsJson}
collapsedCommentIdsJson={nativeBridge.collapsedCommentIdsJson}
contentWidth={NATIVE_REVIEW_DIFF_CONTENT_WIDTH}
rowHeight={NATIVE_REVIEW_DIFF_ROW_HEIGHT}
rowsJson={nativeBridge.rowsJson}
selectedRowIdsJson={nativeBridge.selectedRowIdsJson}
styleJson={nativeBridge.styleJson}
themeJson={nativeBridge.themeJson}
tokensPatchJson={nativeBridge.tokensPatchJson}
tokensResetKey={nativeBridge.tokensResetKey}
viewedFileIdsJson={nativeBridge.viewedFileIdsJson}
onDebug={nativeBridge.onDebug}
onPressLine={commentSelection.onPressLine}
onToggleComment={nativeBridge.onToggleComment}
onToggleFile={handleNativeToggleFile}
onToggleViewedFile={handleNativeToggleViewedFile}
/>
</View>
</View>
) : (
<ScrollView
contentInsetAdjustmentBehavior="never"
contentInset={{ top: topContentInset, bottom: Math.max(insets.bottom, 18) + 18 }}
contentOffset={{ x: 0, y: -topContentInset }}
scrollIndicatorInsets={{
top: topContentInset,
bottom: Math.max(insets.bottom, 18) + 18,
}}
showsVerticalScrollIndicator={false}
style={{ flex: 1 }}
>
{listHeader}
{!selectedSection ? (
<View className="border-b border-border bg-card px-4 py-5">
<Text className="text-sm font-t3-bold text-foreground">No review diffs</Text>
<Text className="text-xs leading-[18px] text-foreground-muted">
This thread has no ready turn diffs and the worktree diff is empty.
</Text>
</View>
) : selectedSection.isLoading && selectedSection.diff === null ? (
<View className="items-center gap-3 border-b border-border bg-card px-4 py-6">
<ActivityIndicator size="small" />
<Text className="text-xs text-foreground-muted">Loading diff…</Text>
</View>
) : parsedDiff.kind === "empty" ? (
<View className="border-b border-border bg-card px-4 py-5">
<Text className="text-sm font-t3-bold text-foreground">No changes</Text>
<Text className="text-xs leading-[18px] text-foreground-muted">
{selectedSection.subtitle ?? "This diff is empty."}
</Text>
</View>
) : parsedDiff.kind === "raw" ? (
<View className="gap-3 border-b border-border bg-card px-4 py-4">
<Text className="text-xs leading-[18px] text-foreground-muted">
{parsedDiff.reason}
</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} bounces={false}>
<Text selectable className="font-mono text-xs leading-[19px] text-foreground">
{parsedDiff.text}
</Text>
</ScrollView>
</View>
) : null}
</ScrollView>
)}
<ReviewSelectionActionBar
bottomInset={insets.bottom}
title={commentSelection.selectionAction?.title ?? null}
onOpenComment={commentSelection.selectionAction?.onOpenComment ?? null}
onClear={commentSelection.clearSelection}
/>
</View>
</>
);
}