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.
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.mdis the easy case;package.json,.gitignore,Dockerfile,tsconfig.jsonandCHANGELOG.mdwill all do it. Both changes are perfectly legitimate — they are different files in different repositories.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 —
renderableGroupskeeps them grouped per repo root and draws a section header per repo, as the comment atapps/web/src/components/DiffPanel.tsx:653-655describes.The file tree does not use that grouping. It is built from the flattened file list:
apps/web/src/components/DiffPanel.tsx:646renderableFiles(DiffPanel.tsx:610-625) is a flat, sorted list keyed onresolveFileDiffPath(file)— the repo-relative path, with nothing identifying which root it came from. Two roots that changedREADME.mdtherefore produce two entries with the identical path, andPathStorerejects the second.DiffFileTree.tsxand the underlyingFileTreeare 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
renderableFilescarries no repo identity, and thatDiffFileTree.tsxis byte-identical to upstream.diffFileTreeEntriesorPathStoreoffers a supported way to disambiguate other than changing the path, and whether the pull request code tab (PullRequestCodeTab.tsx:1408, the otherDiffFileTreecaller) has the same exposure.Reported from Claude Opus 4.6 running in Claude Code.