Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions apps/mobile/src/features/review/ReviewSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ function ReviewHeader(
id: "sections",
inline: true,
items: [
sectionAction(props.sectionMenu.workingTree, "Working tree"),
sectionAction(props.sectionMenu.branchChanges, "Branch changes"),
sectionAction(props.sectionMenu.branchChanges, "Changes"),
sectionAction(props.sectionMenu.workingTree, "Uncommitted"),
sectionAction(props.sectionMenu.latestTurn, "Latest turn"),
],
},
Expand Down
35 changes: 26 additions & 9 deletions apps/mobile/src/features/review/reviewModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe("buildReviewSectionItems", () => {
{
id: "working-tree",
kind: "working-tree",
title: "Dirty worktree",
title: "Uncommitted",
baseRef: "HEAD",
headRef: null,
diff: "diff --git a/a.ts b/a.ts",
Expand All @@ -72,7 +72,7 @@ describe("buildReviewSectionItems", () => {
{
id: "branch-range",
kind: "branch-range",
title: "Against main",
title: "Changes vs main",
baseRef: "main",
headRef: "feature",
diff: "diff --git a/a.ts b/a.ts",
Expand Down Expand Up @@ -105,10 +105,28 @@ describe("buildReviewSectionItems", () => {
isLoading: false,
diff: expect.stringContaining("loaded.ts"),
});
expect(getDefaultReviewSectionId(items)).toBe("turn:2");
expect(getDefaultReviewSectionId(items)).toBe("git:branch-range");
});

it("shows dirty worktree while git preview is loading", () => {
it("falls back to the first turn without git sections", () => {
const items = buildReviewSectionItems({
checkpoints: [
makeCheckpoint({
runId: RunId.make("run-1"),
checkpointTurnCount: 1,
completedAt: "2026-04-01T00:00:00.000Z",
}),
],
gitSections: [],
turnDiffById: {},
loadingTurnIds: {},
loadingGitSections: false,
});

expect(getDefaultReviewSectionId(items)).toBe("turn:1");
});

it("holds the Changes place while git preview is loading", () => {
const items = buildReviewSectionItems({
checkpoints: [],
gitSections: [],
Expand All @@ -119,15 +137,14 @@ describe("buildReviewSectionItems", () => {

expect(items).toEqual([
expect.objectContaining({
id: "git:working-tree",
kind: "working-tree",
title: "Dirty worktree",
subtitle: "Tracked, staged, and untracked worktree changes",
id: "git:branch-range",
kind: "branch-range",
title: "Changes",
diff: null,
isLoading: true,
}),
]);
expect(getDefaultReviewSectionId(items)).toBe("git:working-tree");
expect(getDefaultReviewSectionId(items)).toBe("git:branch-range");
});
});

Expand Down
24 changes: 13 additions & 11 deletions apps/mobile/src/features/review/reviewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import * as Order from "effect/Order";

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

const DIRTY_WORKTREE_SECTION_ID = "git:working-tree";
const DIRTY_WORKTREE_TITLE = "Dirty worktree";
const DIRTY_WORKTREE_SUBTITLE = "Tracked, staged, and untracked worktree changes";
const CHANGES_SECTION_ID = "git:branch-range";
const CHANGES_TITLE = "Changes";
const UNCOMMITTED_SUBTITLE = "Staged, unstaged, and untracked files";

export interface ReviewSectionItem {
readonly id: string;
Expand Down Expand Up @@ -124,7 +124,7 @@ const readyCheckpointOrder = Order.make<ThreadCheckpointSummary>(

function gitSubtitle(section: ReviewDiffPreviewSource): string | null {
if (section.kind === "working-tree") {
return DIRTY_WORKTREE_SUBTITLE;
return UNCOMMITTED_SUBTITLE;
}
if (section.baseRef) {
return `${section.baseRef} ... ${section.headRef ?? "HEAD"}`;
Expand Down Expand Up @@ -443,15 +443,16 @@ export function buildReviewSectionItems(input: {
truncated: section.truncated,
isLoading: false,
}));
const hasDirtyWorktreeItem = gitItems.some((item) => item.id === DIRTY_WORKTREE_SECTION_ID);
// Changes is the default section, so it holds the place while git sources load.
const hasChangesItem = gitItems.some((item) => item.id === CHANGES_SECTION_ID);
const visibleGitItems =
input.loadingGitSections && !hasDirtyWorktreeItem
input.loadingGitSections && !hasChangesItem
? [
{
id: DIRTY_WORKTREE_SECTION_ID,
kind: "working-tree",
title: DIRTY_WORKTREE_TITLE,
subtitle: DIRTY_WORKTREE_SUBTITLE,
id: CHANGES_SECTION_ID,
kind: "branch-range",
title: CHANGES_TITLE,
subtitle: null,
diff: null,
isLoading: true,
} satisfies ReviewSectionItem,
Expand All @@ -462,10 +463,11 @@ export function buildReviewSectionItems(input: {
return [...turnItems, ...visibleGitItems];
}

/** Prefers Changes, then the first section (a turn when the project is not a git repo). */
export function getDefaultReviewSectionId(
sections: ReadonlyArray<ReviewSectionItem>,
): string | null {
return sections[0]?.id ?? null;
return (sections.find((section) => section.id === CHANGES_SECTION_ID) ?? sections[0])?.id ?? null;
}

export function buildReviewParsedDiff(
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/threads/git/GitOverviewSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
<SheetListRow
icon="text.bubble"
title="Review changes"
subtitle="Inspect turn diffs, worktree changes, and base branch diff"
subtitle="Inspect changes, uncommitted edits, and turn diffs"
disabled={busy || !isRepo}
onPress={() => {
const params = { environmentId, threadId };
Expand Down
7 changes: 4 additions & 3 deletions apps/server/src/git/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,7 @@ export const make = Effect.gen(function* () {
const canonicalizeExistingPath = (value: string) =>
fileSystem.realPath(value).pipe(Effect.orElseSucceed(() => value));
const normalizeStatusCacheKey = canonicalizeExistingPath;
const nonRepositoryStatusDetails = {
const nonRepositoryStatusDetails: GitVcsDriver.GitStatusDetails = {
isRepo: false,
hasOriginRemote: false,
isDefaultBranch: false,
Expand All @@ -1012,10 +1012,10 @@ export const make = Effect.gen(function* () {
aheadCount: 0,
behindCount: 0,
aheadOfDefaultCount: 0,
} satisfies GitVcsDriver.GitStatusDetails;
};
const readLocalStatus = Effect.fn("readLocalStatus")(function* (cwd: string) {
const details = yield* gitCore
.statusDetailsLocal(cwd, { includeDivergence: false })
.statusDetailsLocal(cwd, { includeDivergence: false, includeBranchChanges: true })
.pipe(
Effect.catchIf(isNotGitRepositoryError, () => Effect.succeed(nonRepositoryStatusDetails)),
);
Expand All @@ -1031,6 +1031,7 @@ export const make = Effect.gen(function* () {
refName: details.branch,
hasWorkingTreeChanges: details.hasWorkingTreeChanges,
workingTree: details.workingTree,
...(details.branchChanges ? { branchChanges: details.branchChanges } : {}),
} satisfies VcsStatusLocalResult;
});
const localStatusResultCache = yield* Cache.makeWith(readLocalStatus, {
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface GitStatusDetails {
upstreamRef: string | null;
hasWorkingTreeChanges: boolean;
workingTree: VcsStatusResult["workingTree"];
branchChanges?: VcsStatusResult["branchChanges"];
hasUpstream: boolean;
aheadCount: number;
behindCount: number;
Expand All @@ -85,6 +86,8 @@ export interface GitStatusDetails {
export interface GitLocalStatusOptions {
/** Skip revision walks and return zero divergence counts for local-only consumers. */
readonly includeDivergence?: boolean;
/** Also read the diff panel's Changes totals. Failures leave them out. */
readonly includeBranchChanges?: boolean;
}

export interface GitRemoteStatusDetails {
Expand Down
Loading
Loading