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 1/2] #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 { From f9dafed49d54ece225747ab6188296199d54f0ee Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Wed, 22 Jul 2026 19:13:17 -0400 Subject: [PATCH 2/2] fix(ui): distinguish root files without reordering --- .changeset/mark-sidebar-root-groups.md | 5 +++ .changeset/sidebar-root-files-first.md | 5 --- src/ui/AppHost.interactions.test.tsx | 6 +-- src/ui/components/ui-components.test.tsx | 11 +++++- src/ui/lib/files.test.ts | 50 +++++++++++++----------- src/ui/lib/files.ts | 32 +++++---------- src/ui/lib/reviewState.test.ts | 20 ---------- src/ui/lib/reviewState.ts | 3 +- 8 files changed, 55 insertions(+), 77 deletions(-) create mode 100644 .changeset/mark-sidebar-root-groups.md delete mode 100644 .changeset/sidebar-root-files-first.md diff --git a/.changeset/mark-sidebar-root-groups.md b/.changeset/mark-sidebar-root-groups.md new file mode 100644 index 00000000..8a753edc --- /dev/null +++ b/.changeset/mark-sidebar-root-groups.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": patch +--- + +Label repo-root file runs with an in-place `./` sidebar header so root files remain visually distinct without changing review order. diff --git a/.changeset/sidebar-root-files-first.md b/.changeset/sidebar-root-files-first.md deleted file mode 100644 index 2355d5e8..00000000 --- a/.changeset/sidebar-root-files-first.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"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 cd3282eb..ddef9003 100644 --- a/src/ui/AppHost.interactions.test.tsx +++ b/src/ui/AppHost.interactions.test.tsx @@ -411,7 +411,7 @@ function createCollapsedTopBootstrap(): AppBootstrap { ), createTestDiffFile( "second", - "zzz/other.ts", + "other.ts", lines("export const other = 1;"), lines("export const other = 2;"), ), @@ -3299,8 +3299,8 @@ describe("App interactions", () => { } await act(async () => { - // Click inside the second file row in the left sidebar. - await setup.mockMouse.click(6, 4); + // Click inside the second file row below the repo-root group header. + await setup.mockMouse.click(6, 5); }); await flush(setup); diff --git a/src/ui/components/ui-components.test.tsx b/src/ui/components/ui-components.test.tsx index f6f9d092..863dc607 100644 --- a/src/ui/components/ui-components.test.tsx +++ b/src/ui/components/ui-components.test.tsx @@ -441,6 +441,12 @@ describe("UI components", () => { previousPath: "src/ui/Legacy.tsx", stats: { additions: 0, deletions: 0 }, }, + createTestDiffFile( + "root", + "zzz-root.ts", + "export const root = 1;\n", + "export const root = 2;\n", + ), ]; const frame = await captureFrame( { onSelectFile={() => {}} />, 36, - 10, + 12, ); expect(frame).toContain("src/ui/"); expect(frame).toContain("src/core/"); + expect(frame).toContain("./"); + expect(frame).toContain(" zzz-root.ts"); + expect(frame.indexOf("src/ui/")).toBeLessThan(frame.indexOf("./")); expect(frame).toContain(" App.tsx"); expect(frame).toContain(" MenuDropdown.tsx"); expect(frame).toContain(" watch.ts"); diff --git a/src/ui/lib/files.test.ts b/src/ui/lib/files.test.ts index a62efcbc..766edf52 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, hoistRootFilesFirst } from "./files"; +import { buildSidebarEntries, fileLabelParts } from "./files"; describe("files helpers", () => { test("buildSidebarEntries hides zero-value sidebar stats", () => { @@ -117,6 +117,32 @@ describe("files helpers", () => { }); }); + test("buildSidebarEntries marks each root-file run with an in-place ./ group", () => { + const files = [ + createTestDiffFile({ id: "nested-a", path: "src/a.ts" }), + createTestDiffFile({ id: "root-a", path: "README.md" }), + createTestDiffFile({ id: "root-b", path: "package.json" }), + createTestDiffFile({ id: "nested-b", path: "test/b.ts" }), + createTestDiffFile({ id: "root-c", path: "LICENSE" }), + ]; + + const labels = buildSidebarEntries(files).map((entry) => + entry.kind === "group" ? entry.label : entry.name, + ); + + expect(labels).toEqual([ + "src/", + "a.ts", + "./", + "README.md", + "package.json", + "test/", + "b.ts", + "./", + "LICENSE", + ]); + }); + test("fileLabelParts strips parser-added line endings from rename labels", () => { const renamedAcrossDirectories = { ...createTestDiffFile({ @@ -134,26 +160,4 @@ 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 cf6b1b79..eb3a0709 100644 --- a/src/ui/lib/files.ts +++ b/src/ui/lib/files.ts @@ -119,36 +119,22 @@ 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[] = []; - let activeGroup: string | null = null; + let activeGroup: string | undefined; files.forEach((file, index) => { const path = sanitizeTerminalLine(normalizeDiffPath(file.path) ?? file.path); const group = dirname(path); - const nextGroup = group === "." ? null : group; - - if (nextGroup !== activeGroup) { - activeGroup = nextGroup; - if (activeGroup) { - entries.push({ - kind: "group", - id: `group:${activeGroup}:${index}`, - label: `${activeGroup}/`, - }); - } + + if (group !== activeGroup) { + activeGroup = group; + entries.push({ + kind: "group", + id: `group:${group}:${index}`, + label: group === "." ? "./" : `${group}/`, + }); } const agentCommentCount = file.agent?.annotations.length ?? 0; diff --git a/src/ui/lib/reviewState.test.ts b/src/ui/lib/reviewState.test.ts index eef604e7..0b3c4710 100644 --- a/src/ui/lib/reviewState.test.ts +++ b/src/ui/lib/reviewState.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test } from "bun:test"; import { createTestAgentFileContext, createTestDiffFile } from "../../../test/helpers/diff-helpers"; import { - buildReviewState, buildSelectedHunkSummary, findNextAnnotatedFile, resolveReviewNavigationTarget, @@ -115,23 +114,4 @@ 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 06349a64..794a74da 100644 --- a/src/ui/lib/reviewState.ts +++ b/src/ui/lib/reviewState.ts @@ -12,7 +12,6 @@ import type { NavigateToHunkToolInput, SelectedHunkSummary } from "../../hunk-se import { buildSidebarEntries, filterReviewFiles, - hoistRootFilesFirst, mergeFileAnnotationsByFileId, type SidebarEntry, } from "./files"; @@ -56,7 +55,7 @@ export function buildReviewState({ selectedHunkIndex, }: BuildReviewStateOptions): ReviewState { const allFiles = mergeFileAnnotationsByFileId(files, liveCommentsByFileId); - const visibleFiles = hoistRootFilesFirst(filterReviewFiles(allFiles, filterQuery)); + const visibleFiles = filterReviewFiles(allFiles, filterQuery); const selectedFile = resolveSelectedFile(allFiles, visibleFiles, selectedFileId); return {