Skip to content

Diff file tree crashes with "Duplicate path" when two repos change a file with the same path #240

Description

Symptom

In a multi-repo thread, opening the file tree while viewing a turn diff throws and leaves the conversation in a broken state that persists across reloads.

It happens whenever two repo roots in the same diff changed a file at the same repo-relative path. README.md is the easy case; package.json, .gitignore, Dockerfile, tsconfig.json and CHANGELOG.md will all do it. Both changes are perfectly legitimate — they are different files in different repositories.

Error: Duplicate path: "README.md"
    at PathStoreBuilder.appendPreparedPath (FileTree-KDFQbFne.js:1444:64)
    at PathStoreBuilder.appendPreparedPaths (FileTree-KDFQbFne.js:1199:3)
    at new PathStore (FileTree-KDFQbFne.js:3347:17)
    at FileTreeController.resetPaths (FileTree-KDFQbFne.js:4882:38)
    at FileTree.resetPaths (FileTree-KDFQbFne.js:9077:20)
    at t11 (apps/web/src/components/diffs/DiffFileTree.tsx:134:11)

T3 Code (Dev) 0.0.38, multi-repo-workspaces @ a905a0c46, remote SSH environment, project spanning 16 repo roots.

Root cause

The diff panel renders multi-repo diffs correctly — renderableGroups keeps them grouped per repo root and draws a section header per repo, as the comment at apps/web/src/components/DiffPanel.tsx:653-655 describes.

The file tree does not use that grouping. It is built from the flattened file list:

const fileTreeEntries = useMemo(() => diffFileTreeEntries(renderableFiles), [renderableFiles]);

apps/web/src/components/DiffPanel.tsx:646

renderableFiles (DiffPanel.tsx:610-625) is a flat, sorted list keyed on resolveFileDiffPath(file) — the repo-relative path, with nothing identifying which root it came from. Two roots that changed README.md therefore produce two entries with the identical path, and PathStore rejects the second.

DiffFileTree.tsx and the underlying FileTree are unmodified from upstream, where one project is one repo and a path is unique by construction. The assumption is sound there; it is the multi-repo call site that breaks it.

Suggested direction

Namespace tree paths by repo when a diff spans more than one root, so the tree mirrors the grouped rendering the panel already does. repoRootBaseName (DiffPanel.tsx:101-110) already exists for exactly this labelling job and is stable between a thread worktree and the project checkout.

Single-root threads should keep their current flat paths so nothing changes there.

Note on severity

The thrown error leaves the conversation unusable rather than degrading the tree, and it survives a reload, so the practical impact is larger than a broken panel. Whether the tree can fail soft is a separate question from the namespacing.

Verified vs not

  • Verified: the stack trace, the call site, that renderableFiles carries no repo identity, and that DiffFileTree.tsx is byte-identical to upstream.
  • Not verified: whether diffFileTreeEntries or PathStore offers a supported way to disambiguate other than changing the path, and whether the pull request code tab (PullRequestCodeTab.tsx:1408, the other DiffFileTree caller) has the same exposure.

Reported from Claude Opus 4.6 running in Claude Code.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions