Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/mobile/src/connection/environment-cache-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const SHELL_SNAPSHOT: OrchestrationV2ShellSnapshot = {
repositoryIdentity: null,
defaultModelSelection: null,
scripts: [],
trackedFolders: [],
createdAt: "2026-07-29T12:00:00.000Z",
updatedAt: "2026-07-29T12:00:00.000Z",
},
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/home/homeThreadList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function makeProject(
repositoryIdentity: null,
defaultModelSelection: null,
scripts: [],
trackedFolders: [],
createdAt: "2026-06-01T00:00:00.000Z",
updatedAt: "2026-06-01T00:00:00.000Z",
...input,
Expand Down
47 changes: 42 additions & 5 deletions apps/mobile/src/features/review/ReviewSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
import { AppBar } from "../../components/ui/AppBar";
import type { AppBarMenuItem } from "../../components/ui/AppBar.types";
import type { ReviewSectionItem } from "./reviewModel";
import type { ReviewRepository } from "./reviewRepositories";
import { useReviewHeaderPresentation } from "./useReviewHeaderPresentation";
import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens";
import { type Ref, type ReactElement, useImperativeHandle, useRef, useState } from "react";
Expand Down Expand Up @@ -42,7 +43,6 @@ import {
useAdaptiveWorkspacePaneRole,
useRegisterWorkspaceInspector,
} from "../layout/AdaptiveWorkspaceLayout";
import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";
import { useReviewCacheForThread } from "./reviewState";
import {
isNativeReviewDiffDrawEvent,
Expand All @@ -69,6 +69,10 @@ function ReviewHeader(
readonly sectionMenu: ReturnType<typeof buildReviewSectionMenu>;
readonly showSectionToolbar: boolean;
readonly showChangedFilesToggle: boolean;
/** Multi-repo threads only. */
readonly repositories: ReadonlyArray<ReviewRepository>;
readonly repository: ReviewRepository | null;
readonly onSelectRepository: (repository: ReviewRepository) => void;
readonly onSelectSection: (sectionId: string) => void;
readonly onReturnToThread: () => void;
},
Expand Down Expand Up @@ -114,6 +118,21 @@ function ReviewHeader(
title: "Select diff",
icon: presentation.menuIcon,
items: [
...(props.repositories.length > 1
? [
{
id: "repositories",
title: "Repository",
icon: "folder",
items: props.repositories.map((repository) => ({
id: `repository:${repository.cwd}`,
title: repository.name,
selected: repository.cwd === props.repository?.cwd,
onPress: () => props.onSelectRepository(repository),
})),
},
]
: []),
{
id: "sections",
inline: true,
Expand Down Expand Up @@ -420,6 +439,9 @@ function ReviewFileNavigator({
type ReviewSheetProps = StaticScreenProps<{
readonly environmentId: EnvironmentId;
readonly threadId: ThreadId;
/** A repo the agent edited outside the thread; review lists it next to the thread's repos. */
readonly outsideRepositoryRoot?: string;
readonly outsideRepositoryName?: string;
}>;

export function ReviewSheet(props: ReviewSheetProps) {
Expand All @@ -431,13 +453,13 @@ export function ReviewSheet(props: ReviewSheetProps) {
const insets = useSafeAreaInsets();
const { themeAppearance: selectedTheme } = useAppearancePreferences();
const headerIcon = String(useUniwindTheme()["--color-icon"]);
const { environmentId, threadId } = props.route.params;
const { environmentId, threadId, outsideRepositoryRoot, outsideRepositoryName } =
props.route.params;
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 { selectedThreadCwd } = useSelectedThreadWorktree();
const columnMetrics = useNativeColumnLayoutMetrics();
const safeArea = columnMetrics?.safeArea ?? insets;
const topContentInset = Platform.OS === "ios" ? safeArea.top : 0;
Expand All @@ -451,11 +473,18 @@ export function ReviewSheet(props: ReviewSheetProps) {
refreshSelectedSection,
selectSection,
isSelectedSectionPending,
repositories,
repository,
cwd: reviewCwd,
selectRepository,
} = useReviewSections({
enabled: isEnvironmentReady,
environmentId,
threadId,
reviewCache,
outsideRepository: outsideRepositoryRoot
? { root: outsideRepositoryRoot, name: outsideRepositoryName ?? outsideRepositoryRoot }
: null,
});
useReviewDiffPrewarming({
threadKey: reviewCache.threadKey,
Expand All @@ -472,7 +501,7 @@ export function ReviewSheet(props: ReviewSheetProps) {
} = useReviewDiffData({
threadKey: reviewCache.threadKey,
environmentId,
cwd: selectedThreadCwd,
cwd: reviewCwd,
selectedSection,
draftMessage,
});
Expand Down Expand Up @@ -669,11 +698,19 @@ export function ReviewSheet(props: ReviewSheetProps) {
subtitle={headerSubtitle}
androidSubtitle={androidHeaderSubtitle}
iconColor={headerIcon}
selectedThreadCwd={selectedThreadCwd}
selectedThreadCwd={reviewCwd}
gitRepository={
repository === null
? undefined
: { cwd: repository.cwd, home: repository.history === "home" }
}
sectionMenu={sectionMenu}
selectedSection={selectedSection}
showSectionToolbar={showSectionToolbar}
showChangedFilesToggle={showChangedFilesToggle}
repositories={repositories}
repository={repository}
onSelectRepository={selectRepository}
onRefresh={handlePullToRefresh}
onSelectSection={selectSection}
onReturnToThread={handleReturnToThread}
Expand Down
39 changes: 28 additions & 11 deletions apps/mobile/src/features/review/reviewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import * as Arr from "effect/Array";
import { pipe } from "effect/Function";
import * as Order from "effect/Order";

import {
reviewRepositorySectionPrefix,
turnDiffForReviewRepository,
type ReviewRepository,
type ReviewTurnDiff,
} from "./reviewRepositories";

export type ReviewSectionKind = "turn" | "working-tree" | "branch-range";

const CHANGES_SECTION_ID = "git:branch-range";
Expand Down Expand Up @@ -501,29 +508,39 @@ export function getReadyReviewCheckpoints(

export function reviewTurnDiffsBySectionId(
checkpoints: ReadonlyArray<Pick<ThreadCheckpointSummary, "checkpointTurnCount">>,
diffByTurnCount: Readonly<Record<number, string | undefined>>,
diffByTurnCount: Readonly<Record<number, ReviewTurnDiff | undefined>>,
repository: ReviewRepository | null = null,
): Readonly<Record<string, string | undefined>> {
const prefix = reviewRepositorySectionPrefix(repository);
return Object.fromEntries(
checkpoints.map((checkpoint) => [
getReviewSectionIdForCheckpoint(checkpoint),
diffByTurnCount[checkpoint.checkpointTurnCount],
`${prefix}${getReviewSectionIdForCheckpoint(checkpoint)}`,
turnDiffForReviewRepository(diffByTurnCount[checkpoint.checkpointTurnCount], repository),
]),
);
}

/**
* `idPrefix` and `titleSuffix` name a tracked repo in multi-repo threads; the title carries the
* repo into inline review comments.
*/
export function buildReviewSectionItems(input: {
readonly checkpoints: ReadonlyArray<ThreadCheckpointSummary>;
readonly gitSections: ReadonlyArray<ReviewDiffPreviewSource>;
readonly turnDiffById: Readonly<Record<string, string | undefined>>;
readonly loadingGitSections: boolean;
readonly idPrefix?: string;
readonly titleSuffix?: string;
}): ReadonlyArray<ReviewSectionItem> {
const idPrefix = input.idPrefix ?? "";
const titleSuffix = input.titleSuffix ?? "";
const turnItems = getReadyReviewCheckpoints(input.checkpoints).map<ReviewSectionItem>(
(checkpoint) => {
const id = getReviewSectionIdForCheckpoint(checkpoint);
const id = `${idPrefix}${getReviewSectionIdForCheckpoint(checkpoint)}`;
return {
id,
kind: "turn",
title: checkpointTitle(checkpoint),
title: `${checkpointTitle(checkpoint)}${titleSuffix}`,
subtitle: checkpointSubtitle(checkpoint),
diff: input.turnDiffById[id] ?? null,
isLoading: false,
Expand All @@ -532,24 +549,24 @@ export function buildReviewSectionItems(input: {
);

const gitItems = input.gitSections.map<ReviewSectionItem>((section) => ({
id: `git:${section.kind}`,
id: `${idPrefix}git:${section.kind}`,
kind: section.kind,
title: section.title,
title: `${section.title}${titleSuffix}`,
subtitle: gitSubtitle(section),
diff: section.diff,
source: section,
...(section.files ? { files: section.files } : {}),
truncated: section.truncated,
isLoading: false,
}));
const hasChangesItem = gitItems.some((item) => item.id === CHANGES_SECTION_ID);
const hasChangesItem = gitItems.some((item) => item.kind === "branch-range");
const visibleGitItems =
input.loadingGitSections && !hasChangesItem
? [
{
id: CHANGES_SECTION_ID,
id: `${idPrefix}${CHANGES_SECTION_ID}`,
kind: "branch-range",
title: CHANGES_TITLE,
title: `${CHANGES_TITLE}${titleSuffix}`,
subtitle: null,
diff: null,
isLoading: true,
Expand All @@ -564,7 +581,7 @@ export function buildReviewSectionItems(input: {
export function getDefaultReviewSectionId(
sections: ReadonlyArray<ReviewSectionItem>,
): string | null {
return (sections.find((section) => section.id === CHANGES_SECTION_ID) ?? sections[0])?.id ?? null;
return (sections.find((section) => section.kind === "branch-range") ?? sections[0])?.id ?? null;
}

/**
Expand Down
Loading
Loading