Summary
When an environment's path is a subdirectory of the git repository that contains it (a package inside a monorepo, for example), the Changes panel lists the changed files correctly but every file shows "No renderable diff", and expanding context shows "Couldn't load surrounding context". Environments rooted at the repository top level are unaffected.
Versions and environment
- bb 0.43.1, desktop app, macOS (Apple Silicon)
- git 2.55
- Both code paths below are unchanged on
main as of 0bb64f3. I reproduced on the 0.43.1 release and confirmed the cause by reading main. I did not run a build of main.
Steps to reproduce
mkdir -p /tmp/bb-subdir-repro/packages/app && cd /tmp/bb-subdir-repro
git init -q -b main
echo one > packages/app/file.txt
git add -A && git commit -q -m init
echo two >> packages/app/file.txt
- Add a project whose checkout path is
/tmp/bb-subdir-repro/packages/app (the subdirectory, not the repo root).
- Start a thread in it and open the Changes panel.
- Select
packages/app/file.txt.
Expected vs actual
Expected: the one-line diff for file.txt, with expandable context.
Actual: the file is listed as modified, its diff body says "No renderable diff", and context expansion says "Couldn't load surrounding context".
Evidence
The mismatch reproduces with git alone:
cd /tmp/bb-subdir-repro/packages/app
# The file listing is repo-root-relative even though cwd is the subdirectory:
git diff --name-status HEAD --
# M packages/app/file.txt
# bb then asks for that path as a pathspec, which git resolves against cwd:
git diff HEAD -- packages/app/file.txt | wc -c
# 0
# Anchoring the pathspec at the top of the working tree fixes it:
git diff HEAD -- ':(top,literal)packages/app/file.txt' | wc -c
# 169
1. Empty patches. packages/host-workspace/src/workspace.ts runs every git command with cwd: this.path. The changed-file listing (--name-status, --numstat) prints paths relative to the repository root regardless of cwd. Those paths are then passed back as pathspecs through withDiffPathspec, and git resolves a plain pathspec relative to cwd. In a subdirectory environment packages/app/file.txt is looked up as packages/app/packages/app/file.txt, matches nothing, and the patch comes back empty. Because zero sections looks the same as zero entries, the per-file fallback never runs.
2. "Couldn't load surrounding context". routes.diffFile in apps/server/src/routes/environments.ts builds the working-tree path as path.join(environment.path, repoRelativePath). That is the same double prefix, so host.read_file returns ENOENT for the working-tree side. The committed side works, because git show <ref>:<path> is already root-relative.
Proposed fix
For (1), anchor the pathspecs at the top of the working tree. :(top) makes the pathspec independent of cwd, and literal matches what the file already does for its other pathspecs (:(literal)${relativePath}), so paths containing [, * or ? keep working.
--- a/packages/host-workspace/src/workspace.ts
+++ b/packages/host-workspace/src/workspace.ts
@@ function withDiffPathspec(
const withoutSeparator =
args[args.length - 1] === "--" ? args.slice(0, -1) : args;
- return [...withoutSeparator, "--", ...paths];
+ return [
+ ...withoutSeparator,
+ "--",
+ ...paths.map((path) => `:(top,literal)${path}`),
+ ];
}
This is a no-op for every environment rooted at the repository top level: there, top-of-tree and cwd are the same directory, and I checked that the two forms produce byte-identical output. It only changes behaviour in the case that is currently broken. The other two :(literal) call sites in the same file (the index diff loop and the ls-files --others lookup) have the same cwd assumption and would want :(top,literal) plus ls-files --full-name to be consistent.
For (2), the smallest change I found is in the daemon's host.read_file handler: when the read fails with ENOENT, a rootPath was supplied and there is no ref, resolve git -C <rootPath> rev-parse --show-toplevel and retry with join(top, relative(rootPath, path)). It only runs on a read that would otherwise fail, so existing behaviour is untouched. The cleaner alternative is for the server to send the repo-relative path and let the daemon resolve it against the repository root, but that changes the command contract.
What you ruled out
- Not a large-repo or timeout problem: the two-file repository above reproduces it.
- Not a rendering problem: the patch is already empty when it leaves the host daemon.
- Not specific to worktrees, a custom environment provider, or a merge-base setting: a plain project pointed at a subdirectory is enough.
- Top-level environments: the proposed pathspec form produces byte-identical
git diff output there, so they should see no change.
Checks
AGENT GENERATED. Investigated and drafted with Claude Code, reviewed by me before filing. The thread is local to my machine, so the reproduction above is the full report.
Summary
When an environment's path is a subdirectory of the git repository that contains it (a package inside a monorepo, for example), the Changes panel lists the changed files correctly but every file shows "No renderable diff", and expanding context shows "Couldn't load surrounding context". Environments rooted at the repository top level are unaffected.
Versions and environment
mainas of 0bb64f3. I reproduced on the 0.43.1 release and confirmed the cause by readingmain. I did not run a build ofmain.Steps to reproduce
/tmp/bb-subdir-repro/packages/app(the subdirectory, not the repo root).packages/app/file.txt.Expected vs actual
Expected: the one-line diff for
file.txt, with expandable context.Actual: the file is listed as modified, its diff body says "No renderable diff", and context expansion says "Couldn't load surrounding context".
Evidence
The mismatch reproduces with git alone:
1. Empty patches.
packages/host-workspace/src/workspace.tsruns every git command withcwd: this.path. The changed-file listing (--name-status,--numstat) prints paths relative to the repository root regardless of cwd. Those paths are then passed back as pathspecs throughwithDiffPathspec, and git resolves a plain pathspec relative to cwd. In a subdirectory environmentpackages/app/file.txtis looked up aspackages/app/packages/app/file.txt, matches nothing, and the patch comes back empty. Because zero sections looks the same as zero entries, the per-file fallback never runs.2. "Couldn't load surrounding context".
routes.diffFileinapps/server/src/routes/environments.tsbuilds the working-tree path aspath.join(environment.path, repoRelativePath). That is the same double prefix, sohost.read_filereturns ENOENT for the working-tree side. The committed side works, becausegit show <ref>:<path>is already root-relative.Proposed fix
For (1), anchor the pathspecs at the top of the working tree.
:(top)makes the pathspec independent of cwd, andliteralmatches what the file already does for its other pathspecs (:(literal)${relativePath}), so paths containing[,*or?keep working.This is a no-op for every environment rooted at the repository top level: there, top-of-tree and cwd are the same directory, and I checked that the two forms produce byte-identical output. It only changes behaviour in the case that is currently broken. The other two
:(literal)call sites in the same file (the index diff loop and thels-files --otherslookup) have the same cwd assumption and would want:(top,literal)plusls-files --full-nameto be consistent.For (2), the smallest change I found is in the daemon's
host.read_filehandler: when the read fails with ENOENT, arootPathwas supplied and there is noref, resolvegit -C <rootPath> rev-parse --show-topleveland retry withjoin(top, relative(rootPath, path)). It only runs on a read that would otherwise fail, so existing behaviour is untouched. The cleaner alternative is for the server to send the repo-relative path and let the daemon resolve it against the repository root, but that changes the command contract.What you ruled out
git diffoutput there, so they should see no change.Checks
main, or I say above that I could not.> AGENT GENERATEDand links the thread or report.