From 116293d66aee2aab8980e2384990de3a2c1887f5 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 22 Aug 2026 03:02:13 +0100 Subject: [PATCH 1/2] fix(mobile): stop the changed files widget from crashing ReviewSheet force-unwrapped resolveNativeReviewDiffView(), which is documented to return null while Expo registers the native view config and forever after a failed requireNativeView (e.g. a binary without the T3ReviewDiffSurface module). Rendering a null component type throws "Element type is invalid", which is fatal in release builds: the app crashes the moment the diff widget appears, and since the thread's review section auto-selects from persisted checkpoints, every reopen crashes again and the chat is burned. Null-check the resolver like ThreadFeed's ReviewCommentCard already does, and fall back to showing the raw patch when the native surface is unavailable. Fixes #7800 ox-alpha via opencode --- .../src/features/review/ReviewSheet.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index 80ebe1157d92..adcb230f7ce1 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -401,7 +401,11 @@ export function ReviewSheet(props: ReviewSheetProps) { selectedSection, draftMessage, }); - const NativeReviewDiffView = resolveNativeReviewDiffView()!; + // Resolution returns null while Expo registers the native view (or forever + // when the binary lacks it). Rendering a null component type crashes the + // app, so callers must fall back — ThreadFeed's ReviewCommentCard does the + // same check. + const NativeReviewDiffView = resolveNativeReviewDiffView(); const nativeReviewDiffViewRef = useRef(null); const showcasedReviewDrawRef = useRef(null); // Native pull-to-refresh on the diff surface (replaces the old Refresh menu item). @@ -783,7 +787,7 @@ export function ReviewSheet(props: ReviewSheetProps) { onRetry={handleRetryEnvironment} /> - ) : selectedSection && parsedDiff.kind === "files" ? ( + ) : selectedSection && parsedDiff.kind === "files" && NativeReviewDiffView ? ( + ) : parsedDiff.kind === "files" ? ( + // The native diff surface could not be resolved on this binary; + // degrade to the raw patch instead of crashing the app. + + + Native diff view unavailable. Showing the raw patch. + + + + {selectedSection?.diff ?? ""} + + + ) : null} )} From 9b9b8597ef16c76cc832d3188a90b7ec529e20ad Mon Sep 17 00:00:00 2001 From: Lars Nieuwenhuis <35393046+lnieuwenhuis@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:20:55 +0200 Subject: [PATCH 2/2] fix(mobile): hide changed-files navigator and restore refresh in raw diff fallback When the native diff surface is unavailable, the inspector navigator could not scroll or filter the raw patch, and iOS had no refresh affordance. Gate the navigator (and its toolbar toggle) on the native view resolving, and add a RefreshControl to the fallback ScrollView. --- .../src/features/review/ReviewSheet.tsx | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index adcb230f7ce1..c1f8f8e6a5f7 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -23,6 +23,7 @@ import { FlatList, Platform, Pressable, + RefreshControl, ScrollView, type NativeSyntheticEvent, StyleSheet, @@ -603,11 +604,27 @@ export function ReviewSheet(props: ReviewSheetProps) { .filter((part): part is string => Boolean(part)) .join(" · "); - // The changed-files navigator lives in the workspace inspector column — - // the single right-hand pane per route — instead of an in-screen panel. + // The changed-files navigator drives the native diff surface via + // scrollToFile, so it is only useful when that surface resolved. In raw + // fallback mode the ref is necessarily null and the raw patch neither + // scrolls nor filters — registering the navigator would present working + // controls that cannot navigate. const showChangedFilesPane = - !showConnectionNotice && selectedSection !== null && parsedDiff.kind === "files"; + !showConnectionNotice && + selectedSection !== null && + parsedDiff.kind === "files" && + NativeReviewDiffView !== null; useRegisterWorkspaceInspector(showChangedFilesPane ? renderInspector : undefined); + // Raw fallback renders the patch inline with no inspector content, so the + // pane toggle would open an empty column — hide it in exactly that case. + const showChangedFilesToggle = + panes.supportsAuxiliaryPane && + !( + !showConnectionNotice && + selectedSection !== null && + parsedDiff.kind === "files" && + NativeReviewDiffView === null + ); const listHeader = useMemo(() => { const children: ReactElement[] = []; @@ -694,7 +711,7 @@ export function ReviewSheet(props: ReviewSheetProps) { {!isAndroid && (showSectionToolbar || panes.supportsAuxiliaryPane || gitMenuAvailable) ? ( - {panes.supportsAuxiliaryPane ? ( + {showChangedFilesToggle ? ( void handlePullToRefresh()} + /> + } > {listHeader} {!selectedSection ? (