Skip to content
57 changes: 38 additions & 19 deletions apps/mobile/src/features/review/ReviewSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -382,24 +382,39 @@ export function ReviewSheet(props: ReviewSheetProps) {
useEffect(() => {
showAuxiliaryPane("inspector");
}, [environmentId, showAuxiliaryPane, threadId]);
const { error, reviewSections, selectedSection, refreshSelectedSection, selectSection } =
useReviewSections({
enabled: isEnvironmentReady,
environmentId,
threadId,
reviewCache,
});
const {
error,
reviewSections,
selectedSection,
refreshSelectedSection,
selectSection,
isSelectedSectionPending,
} = useReviewSections({
enabled: isEnvironmentReady,
environmentId,
threadId,
reviewCache,
});
useReviewDiffPrewarming({
threadKey: reviewCache.threadKey,
sections: reviewSections,
selectedSectionId: selectedSection?.id ?? null,
});
const { headerDiffSummary, nativeReviewDiffData, parsedDiff, pendingReviewCommentCount } =
useReviewDiffData({
threadKey: reviewCache.threadKey,
selectedSection,
draftMessage,
});
const {
headerDiffSummary,
nativeReviewDiffData,
parsedDiff,
pendingReviewCommentCount,
loadVisibleFile,
refreshFilePatches,
isPending: areFilePatchesPending,
} = useReviewDiffData({
threadKey: reviewCache.threadKey,
environmentId,
cwd: selectedThreadCwd,
selectedSection,
draftMessage,
});
// 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
Expand All @@ -412,11 +427,12 @@ export function ReviewSheet(props: ReviewSheetProps) {
const handlePullToRefresh = useCallback(async () => {
setIsPullRefreshing(true);
try {
refreshFilePatches();
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
await refreshSelectedSection();
} finally {
setIsPullRefreshing(false);
}
}, [refreshSelectedSection]);
}, [refreshSelectedSection, refreshFilePatches]);
const reviewFileNavigatorRef = useRef<ReviewFileNavigatorHandle>(null);
const reviewFiles = parsedDiff.kind === "files" ? parsedDiff.files : [];
const fileVisibility = useReviewFileVisibility({
Expand Down Expand Up @@ -469,6 +485,7 @@ export function ReviewSheet(props: ReviewSheetProps) {

const handleSelectFile = useCallback(
(fileId: string | null) => {
loadVisibleFile(fileId, true);
commentSelection.clearSelection();
if (fileId !== null && collapsedFileIds.includes(fileId)) {
toggleExpandedFile(fileId);
Expand All @@ -481,13 +498,14 @@ export function ReviewSheet(props: ReviewSheetProps) {
console.error("[review] Failed to navigate to diff file", error);
});
},
[collapsedFileIds, commentSelection, toggleExpandedFile],
[collapsedFileIds, commentSelection, toggleExpandedFile, loadVisibleFile],
);
const handleVisibleFileChange = useCallback(
(event: NativeSyntheticEvent<{ readonly fileId?: string | null }>) => {
loadVisibleFile(event.nativeEvent.fileId ?? null);
reviewFileNavigatorRef.current?.setVisibleFile(event.nativeEvent.fileId ?? null);
},
[],
[loadVisibleFile],
);
const renderInspector = useCallback(
() => (
Expand All @@ -508,10 +526,11 @@ export function ReviewSheet(props: ReviewSheetProps) {
(event: NativeSyntheticEvent<{ readonly fileId?: string }>) => {
const { fileId } = event.nativeEvent;
if (fileId) {
loadVisibleFile(fileId, true);
toggleExpandedFile(fileId);
}
},
[toggleExpandedFile],
[toggleExpandedFile, loadVisibleFile],
);

const handleNativeToggleViewedFile = useCallback(
Expand Down Expand Up @@ -818,7 +837,7 @@ export function ReviewSheet(props: ReviewSheetProps) {
<NativeReviewDiffView
collapsable={false}
testID="review-native-diff-view"
refreshing={isPullRefreshing}
refreshing={isPullRefreshing || isSelectedSectionPending || areFilePatchesPending}
onPullToRefresh={() => void handlePullToRefresh()}
style={StyleSheet.absoluteFill}
appearanceScheme={selectedTheme}
Expand Down Expand Up @@ -862,7 +881,7 @@ export function ReviewSheet(props: ReviewSheetProps) {
// iOS has no other refresh affordance here (the explicit
// "Refresh current diff" menu is Android-only).
<RefreshControl
refreshing={isPullRefreshing}
refreshing={isPullRefreshing || isSelectedSectionPending || areFilePatchesPending}
onRefresh={() => void handlePullToRefresh()}
/>
}
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/review/nativeReviewDiffAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ function createNoticeRow(fileId: string, suffix: string, text: string): NativeRe
}

function noticeRowsForFile(file: ReviewRenderableFile): ReadonlyArray<NativeReviewDiffRow> {
if (file.notice) return [createNoticeRow(file.id, "loading", file.notice)];
if (file.rows.length > 0) {
return [];
}
Expand Down
32 changes: 32 additions & 0 deletions apps/mobile/src/features/review/reviewModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@t3tools/contracts";

import {
applyReviewDiffMetadata,
buildReviewParsedDiff,
buildReviewSectionItems,
getDefaultReviewSectionId,
Expand Down Expand Up @@ -271,3 +272,34 @@ describe("buildReviewParsedDiff", () => {
});
});
});

describe("applyReviewDiffMetadata", () => {
it("uses complete counts even when the preview contains only part of one file", () => {
const parsed = buildReviewParsedDiff(
[
"diff --git a/large.txt b/large.txt",
"--- a/large.txt",
"+++ b/large.txt",
"@@ -1 +1 @@",
"-before",
"+after",
].join("\n"),
"partial",
);
const result = applyReviewDiffMetadata(parsed, {
truncated: true,
files: [
{ path: "large.txt", previousPath: null, additions: 4000, deletions: 3000 },
{ path: "unseen.txt", previousPath: null, additions: 100, deletions: 20 },
],
});
expect(result.kind).toBe("files");
if (result.kind !== "files") return;
expect(result.fileCount).toBe(2);
expect(result.additions).toBe(4100);
expect(result.deletions).toBe(3020);
expect(result.files[0]?.additions).toBe(4000);
expect(result.notice).toContain("Counts include all changes");
expect(applyReviewDiffMetadata(parsed, null)).toEqual(parsed);
});
});
31 changes: 31 additions & 0 deletions apps/mobile/src/features/review/reviewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export interface ReviewSectionItem {
readonly subtitle: string | null;
readonly diff: string | null;
readonly isLoading: boolean;
readonly files?: ReviewDiffPreviewSource["files"];
readonly truncated?: boolean;
readonly source?: ReviewDiffPreviewSource;
}

export interface ReviewRenderableHunkRow {
Expand Down Expand Up @@ -47,6 +50,7 @@ export type ReviewRenderableRow = ReviewRenderableHunkRow | ReviewRenderableLine
export interface ReviewRenderableFile {
readonly id: string;
readonly cacheKey: string;
readonly notice?: string;
readonly path: string;
readonly previousPath: string | null;
readonly changeType: ChangeTypes;
Expand Down Expand Up @@ -442,6 +446,9 @@ export function buildReviewSectionItems(input: {
title: section.title,
subtitle: gitSubtitle(section),
diff: section.diff,
source: section,
...(section.files ? { files: section.files } : {}),
truncated: section.truncated,
isLoading: false,
}));
const hasDirtyWorktreeItem = gitItems.some((item) => item.id === DIRTY_WORKTREE_SECTION_ID);
Expand Down Expand Up @@ -527,3 +534,27 @@ export function buildReviewParsedDiff(
};
}
}

export function applyReviewDiffMetadata(
previewDiff: ReviewParsedDiff,
selectedSection: Pick<ReviewSectionItem, "files" | "truncated"> | null,
): ReviewParsedDiff {
if (previewDiff.kind === "empty") return previewDiff;
const notice = selectedSection?.truncated
? `This preview exceeds the size limit. Changes shown are incomplete.${selectedSection.files ? " Counts include all changes." : ""}`
: previewDiff.notice;
if (previewDiff.kind !== "files" || !selectedSection?.files) return { ...previewDiff, notice };
const totals = selectedSection.files.reduce(
(total, file) => ({
additions: total.additions + file.additions,
deletions: total.deletions + file.deletions,
}),
{ additions: 0, deletions: 0 },
);
const stats = new Map(selectedSection.files.map((file) => [file.path, file]));
const files = previewDiff.files.map((file) => {
const stat = stats.get(file.path);
return stat ? { ...file, additions: stat.additions, deletions: stat.deletions } : file;
});
return { ...previewDiff, ...totals, files, fileCount: selectedSection.files.length, notice };
}
Loading
Loading