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
5 changes: 5 additions & 0 deletions .changeset/sidebar-root-files-first.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Hoist repo-root files above folder groups in the sidebar and file navigation, preserving the existing (VCS or agent-provided) order otherwise. This keeps a repo-root file from rendering as if it belongs to the folder group it would otherwise follow in the sidebar. (#461)
2 changes: 1 addition & 1 deletion src/ui/AppHost.interactions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ function createCollapsedTopBootstrap(): AppBootstrap {
),
createTestDiffFile(
"second",
"other.ts",
"zzz/other.ts",
lines("export const other = 1;"),
lines("export const other = 2;"),
),
Expand Down
24 changes: 23 additions & 1 deletion src/ui/lib/files.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { createTestDiffFile, lines } from "../../../test/helpers/diff-helpers";
import { buildSidebarEntries, fileLabelParts } from "./files";
import { buildSidebarEntries, fileLabelParts, hoistRootFilesFirst } from "./files";

describe("files helpers", () => {
test("buildSidebarEntries hides zero-value sidebar stats", () => {
Expand Down Expand Up @@ -134,4 +134,26 @@ describe("files helpers", () => {
stateLabel: null,
});
});

test("hoistRootFilesFirst moves repo-root files to the top, preserving relative order", () => {
const make = (id: string, path: string) =>
createTestDiffFile({
id,
path,
before: lines("export const stable = true;"),
after: lines("export const stable = true;", `export const ${id} = 1;`),
});
const files = [
make("mid", "internal/mod.ts"),
make("rootB", "readme.ts"),
make("deep", "internal/sub/x.ts"),
make("rootA", "main.ts"),
];

const ordered = hoistRootFilesFirst(files).map((file) => file.path);

// readme.ts stays before main.ts, proving root files are hoisted in their
// original order rather than alphabetically re-sorted.
expect(ordered).toEqual(["readme.ts", "main.ts", "internal/mod.ts", "internal/sub/x.ts"]);
});
});
11 changes: 11 additions & 0 deletions src/ui/lib/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ export function filterReviewFiles(files: DiffFile[], query: string): DiffFile[]
});
}

/** Move repo-root files ahead of folder-nested files, keeping each group's original order. */
export function hoistRootFilesFirst(files: DiffFile[]): DiffFile[] {
const isRootFile = (file: DiffFile) => {
const path = sanitizeTerminalLine(normalizeDiffPath(file.path) ?? file.path);
return dirname(path) === ".";
};
const rootFiles = files.filter(isRootFile);
const folderFiles = files.filter((file) => !isRootFile(file));
return [...rootFiles, ...folderFiles];
}

/** Build the grouped sidebar entries while preserving the review stream order. */
export function buildSidebarEntries(files: DiffFile[]): SidebarEntry[] {
const entries: SidebarEntry[] = [];
Expand Down
20 changes: 20 additions & 0 deletions src/ui/lib/reviewState.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import { createTestAgentFileContext, createTestDiffFile } from "../../../test/helpers/diff-helpers";
import {
buildReviewState,
buildSelectedHunkSummary,
findNextAnnotatedFile,
resolveReviewNavigationTarget,
Expand Down Expand Up @@ -114,4 +115,23 @@ describe("review state helpers", () => {
}),
).toThrow("No diff hunk");
});

// Intent: guard that buildReviewState applies the hoist itself. Dropping the
// wrapper would regress sidebar and nav order while the helper test stays green.
test("buildReviewState hoists repo-root files above folder groups", () => {
const files = [
createTestDiffFile({ id: "nested", path: "src/a.ts" }),
createTestDiffFile({ id: "root", path: "readme.ts" }),
];

const state = buildReviewState({
files,
liveCommentsByFileId: {},
filterQuery: "",
selectedFileId: "",
selectedHunkIndex: 0,
});

expect(state.visibleFiles.map((file) => file.id)).toEqual(["root", "nested"]);
});
});
3 changes: 2 additions & 1 deletion src/ui/lib/reviewState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { NavigateToHunkToolInput, SelectedHunkSummary } from "../../hunk-se
import {
buildSidebarEntries,
filterReviewFiles,
hoistRootFilesFirst,
mergeFileAnnotationsByFileId,
type SidebarEntry,
} from "./files";
Expand Down Expand Up @@ -55,7 +56,7 @@ export function buildReviewState({
selectedHunkIndex,
}: BuildReviewStateOptions): ReviewState {
const allFiles = mergeFileAnnotationsByFileId(files, liveCommentsByFileId);
const visibleFiles = filterReviewFiles(allFiles, filterQuery);
const visibleFiles = hoistRootFilesFirst(filterReviewFiles(allFiles, filterQuery));
const selectedFile = resolveSelectedFile(allFiles, visibleFiles, selectedFileId);

return {
Expand Down