From a6f75d483ae6de1904d255a69f63c1a3a2ff5199 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:25:08 +0800 Subject: [PATCH] fix(app): follow file tree order in review navigation (#46557) --- .../session/files/file-tree-v2-model.test.ts | 29 ++++++++++++++++++- .../src/session/files/file-tree-v2-model.ts | 6 ++++ packages/app/src/session/review/panel.tsx | 8 +++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/app/src/session/files/file-tree-v2-model.test.ts b/packages/app/src/session/files/file-tree-v2-model.test.ts index 6019f485a79..9e14f502b13 100644 --- a/packages/app/src/session/files/file-tree-v2-model.test.ts +++ b/packages/app/src/session/files/file-tree-v2-model.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { buildFileTreeV2Model, flattenFileTreeV2, flattenLiveFileTreeV2 } from "./file-tree-v2-model" +import { buildFileTreeV2Model, flattenFileTreeV2, flattenLiveFileTreeV2, sortFileTreeV2Paths } from "./file-tree-v2-model" import type { FileNode } from "@/runtime/server/types" describe("buildFileTreeV2Model", () => { @@ -45,6 +45,33 @@ describe("buildFileTreeV2Model", () => { }) }) +describe("sortFileTreeV2Paths", () => { + test("orders navigation depth-first with directories before sibling files", () => { + const paths = ["README.md", "src/a.ts", "src/lib/z.ts", "docs/guide.md", "src/lib/b.ts"] + + expect(sortFileTreeV2Paths(paths)).toEqual([ + "docs/guide.md", + "src/lib/b.ts", + "src/lib/z.ts", + "src/a.ts", + "README.md", + ]) + expect(paths[0]).toBe("README.md") + }) + + test("preserves original paths for selection", () => { + expect(sortFileTreeV2Paths(["README.md", "src\\lib\\a.ts", "/docs//guide.md/"])).toEqual([ + "/docs//guide.md/", + "src\\lib\\a.ts", + "README.md", + ]) + }) + + test("handles an empty file list", () => { + expect(sortFileTreeV2Paths([])).toEqual([]) + }) +}) + describe("flattenLiveFileTreeV2", () => { test("flattens live children using original paths for nested lookups", () => { const nodes: Record = { diff --git a/packages/app/src/session/files/file-tree-v2-model.ts b/packages/app/src/session/files/file-tree-v2-model.ts index 0b834864c57..08586ec4070 100644 --- a/packages/app/src/session/files/file-tree-v2-model.ts +++ b/packages/app/src/session/files/file-tree-v2-model.ts @@ -76,6 +76,12 @@ export function flattenFileTreeV2(model: FileTreeV2Model, expanded: (path: strin return rows } +export function sortFileTreeV2Paths(paths: readonly string[]) { + return flattenFileTreeV2(buildFileTreeV2Model(paths), () => true) + .filter((row) => row.node.type === "file") + .map((row) => row.node.originalPath) +} + export function flattenLiveFileTreeV2( children: (path: string) => readonly FileNode[], expanded: (path: string) => boolean, diff --git a/packages/app/src/session/review/panel.tsx b/packages/app/src/session/review/panel.tsx index 7a1352ba8d4..8e47e78d66a 100644 --- a/packages/app/src/session/review/panel.tsx +++ b/packages/app/src/session/review/panel.tsx @@ -18,6 +18,7 @@ import type { SessionReviewLineComment, } from "@opencode-ai/session-ui/session-review" import FileTreeV2 from "@/session/files/file-tree-v2" +import { sortFileTreeV2Paths } from "@/session/files/file-tree-v2-model" import { useLanguage } from "@/runtime/i18n/language" import { useWorkspaceLocation } from "@/workspaces/location" import { useServerSDK } from "@/runtime/server/client" @@ -83,6 +84,9 @@ export function ReviewPanelView( ), ) const searching = createMemo(() => props.state.filter().trim().length > 0) + const navigationFiles = createMemo(() => + searching() || props.fileList === "flat" ? filteredFiles() : sortFileTreeV2Paths(filteredFiles()), + ) const kinds = createMemo(() => reviewDiffKinds(diffs())) // Changes-only trees omit "M" — every row is already a change; A/D stay visible. const treeKinds = createMemo(() => new Map([...kinds()].filter(([, kind]) => kind !== "mix"))) @@ -93,7 +97,7 @@ export function ReviewPanelView( if (focus && diffs().some((diff) => diff.file === focus.file)) return focus.file const active = props.activeFile if (searching()) return active - const files = filteredFiles() + const files = navigationFiles() if (active && files.includes(active)) return active return files[0] }) @@ -141,7 +145,7 @@ export function ReviewPanelView( /> } activeFile={activeDiff()} - files={filteredFiles()} + files={navigationFiles()} onSelectFile={props.onSelectFile} diffStyle={props.diffStyle} onDiffStyleChange={props.onDiffStyleChange}