From 269993a59e00a595683e7d0ad6d883a881b0198f Mon Sep 17 00:00:00 2001 From: BowlOfSoup <19224810+BowlOfSoup@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:33:21 +0200 Subject: [PATCH] #461: Files in the root on top instead of mixed with directories --- .changeset/sidebar-root-files-first.md | 5 +++++ src/ui/AppHost.interactions.test.tsx | 2 +- src/ui/lib/files.test.ts | 24 +++++++++++++++++++++++- src/ui/lib/files.ts | 11 +++++++++++ src/ui/lib/reviewState.test.ts | 20 ++++++++++++++++++++ src/ui/lib/reviewState.ts | 3 ++- 6 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 .changeset/sidebar-root-files-first.md diff --git a/.changeset/sidebar-root-files-first.md b/.changeset/sidebar-root-files-first.md new file mode 100644 index 00000000..2355d5e8 --- /dev/null +++ b/.changeset/sidebar-root-files-first.md @@ -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) diff --git a/src/ui/AppHost.interactions.test.tsx b/src/ui/AppHost.interactions.test.tsx index 94daad81..99e7af77 100644 --- a/src/ui/AppHost.interactions.test.tsx +++ b/src/ui/AppHost.interactions.test.tsx @@ -410,7 +410,7 @@ function createCollapsedTopBootstrap(): AppBootstrap { ), createTestDiffFile( "second", - "other.ts", + "zzz/other.ts", lines("export const other = 1;"), lines("export const other = 2;"), ), diff --git a/src/ui/lib/files.test.ts b/src/ui/lib/files.test.ts index 4e2b653f..a62efcbc 100644 --- a/src/ui/lib/files.test.ts +++ b/src/ui/lib/files.test.ts @@ -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", () => { @@ -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"]); + }); }); diff --git a/src/ui/lib/files.ts b/src/ui/lib/files.ts index 0fdb247b..cf6b1b79 100644 --- a/src/ui/lib/files.ts +++ b/src/ui/lib/files.ts @@ -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[] = []; diff --git a/src/ui/lib/reviewState.test.ts b/src/ui/lib/reviewState.test.ts index 0b3c4710..eef604e7 100644 --- a/src/ui/lib/reviewState.test.ts +++ b/src/ui/lib/reviewState.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { createTestAgentFileContext, createTestDiffFile } from "../../../test/helpers/diff-helpers"; import { + buildReviewState, buildSelectedHunkSummary, findNextAnnotatedFile, resolveReviewNavigationTarget, @@ -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"]); + }); }); diff --git a/src/ui/lib/reviewState.ts b/src/ui/lib/reviewState.ts index 794a74da..06349a64 100644 --- a/src/ui/lib/reviewState.ts +++ b/src/ui/lib/reviewState.ts @@ -12,6 +12,7 @@ import type { NavigateToHunkToolInput, SelectedHunkSummary } from "../../hunk-se import { buildSidebarEntries, filterReviewFiles, + hoistRootFilesFirst, mergeFileAnnotationsByFileId, type SidebarEntry, } from "./files"; @@ -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 {