From 97fc32961e7339aacafea13de9409640d8877c5a Mon Sep 17 00:00:00 2001 From: euxaristia <25621994+euxaristia@users.noreply.github.com> Date: Thu, 9 Jul 2026 07:43:43 -0400 Subject: [PATCH] fix(agent,tui): resolve git branch detection when starting Zero in subdirectories Currently, both gitBranchForPrompt (agent) and gitBranch (tui) check for a .git entry directly inside the current working directory. When starting Zero in nested folders/subdirectories of a repository, the check fails and the branch segment is omitted. Export findProjectGitRoot as FindProjectGitRoot in agent package, and use it in both functions to resolve the correct repository root before checking HEAD and resolving the branch name. Also resolve relative worktree gitdirs relative to the repository root. --- internal/agent/loop_test.go | 18 ++++++++++++++++++ internal/agent/system_prompt.go | 20 ++++++++++++-------- internal/agent/system_prompt_test.go | 4 ++-- internal/tui/tui_fixes_test.go | 18 ++++++++++++++++++ internal/tui/view.go | 15 +++++++++++++-- 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/internal/agent/loop_test.go b/internal/agent/loop_test.go index 04d3c2d12..97647258b 100644 --- a/internal/agent/loop_test.go +++ b/internal/agent/loop_test.go @@ -2947,6 +2947,24 @@ func TestGitBranchForPromptResolvesRelativeWorktreeGitdir(t *testing.T) { } } +func TestGitBranchForPromptInSubdirectory(t *testing.T) { + root := t.TempDir() + gitdir := filepath.Join(root, ".git") + if err := os.MkdirAll(gitdir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(gitdir, "HEAD"), []byte("ref: refs/heads/main\n"), 0o644); err != nil { + t.Fatal(err) + } + subdir := filepath.Join(root, "sub", "dir") + if err := os.MkdirAll(subdir, 0o755); err != nil { + t.Fatal(err) + } + if got := gitBranchForPrompt(subdir); got != "main" { + t.Fatalf("gitBranchForPrompt in subdirectory = %q, want main", got) + } +} + func TestBuildSystemPromptInjectsWorkspaceContext(t *testing.T) { cwd := t.TempDir() if err := os.WriteFile(filepath.Join(cwd, "AGENTS.md"), []byte("Always run `make lint` before committing."), 0o644); err != nil { diff --git a/internal/agent/system_prompt.go b/internal/agent/system_prompt.go index 1d7118edf..d193fd12a 100644 --- a/internal/agent/system_prompt.go +++ b/internal/agent/system_prompt.go @@ -295,7 +295,7 @@ func workspaceContext(cwd string) string { } b.WriteString("") - b.WriteString(projectGuidelines(cwd, findProjectGitRoot(cwd))) + b.WriteString(projectGuidelines(cwd, FindProjectGitRoot(cwd))) if repoMap := repoMapContext(cwd); repoMap != "" { b.WriteString("\n\n## Repo map\n\n" + repoMap) } @@ -542,17 +542,17 @@ func resolveDirCaseInsensitive(target, anchor string) (string, bool) { return cur, true } -// findProjectGitRoot returns the nearest ancestor of cwd that contains a +// FindProjectGitRoot returns the nearest ancestor of cwd that contains a // .git entry (file or directory). Returns "" when no git root is found, so // the caller can fall back to cwd-only lookup. -func findProjectGitRoot(cwd string) string { +func FindProjectGitRoot(cwd string) string { cwd = strings.TrimSpace(cwd) if cwd == "" { return "" } cur := cwd for { - if hasGitMetadata(cur) { + if HasGitMetadata(cur) { return cur } parent := filepath.Dir(cur) @@ -563,7 +563,7 @@ func findProjectGitRoot(cwd string) string { } } -func hasGitMetadata(dir string) bool { +func HasGitMetadata(dir string) bool { gitPath := filepath.Join(dir, ".git") info, err := os.Stat(gitPath) if err != nil { @@ -619,7 +619,11 @@ func repoMapContext(cwd string) string { // cwd, handling both a regular checkout (.git dir) and a worktree (.git file). // Returns "" on any problem — the prompt simply omits the branch segment. func gitBranchForPrompt(cwd string) string { - gitPath := filepath.Join(cwd, ".git") + gitRoot := FindProjectGitRoot(cwd) + if gitRoot == "" { + return "" + } + gitPath := filepath.Join(gitRoot, ".git") info, err := os.Stat(gitPath) if err != nil { return "" @@ -635,10 +639,10 @@ func gitBranchForPrompt(cwd string) string { return "" } // In worktree mode the gitdir is often RELATIVE (e.g. - // "gitdir: ../.git/worktrees/") — resolve it against cwd, not the + // "gitdir: ../.git/worktrees/") — resolve it against the worktree root (gitRoot), not the // process working directory, or HEAD lookup fails and we drop the branch. if !filepath.IsAbs(dir) { - dir = filepath.Join(cwd, dir) + dir = filepath.Join(gitRoot, dir) } headPath = filepath.Join(dir, "HEAD") } diff --git a/internal/agent/system_prompt_test.go b/internal/agent/system_prompt_test.go index 87c1f1d61..fc07ba7b9 100644 --- a/internal/agent/system_prompt_test.go +++ b/internal/agent/system_prompt_test.go @@ -424,7 +424,7 @@ func TestFindProjectGitRootIgnoresEmptyGitDirectory(t *testing.T) { if err := os.MkdirAll(leaf, 0o755); err != nil { t.Fatal(err) } - if got := findProjectGitRoot(leaf); got != "" { - t.Fatalf("findProjectGitRoot = %q, want empty for invalid .git directory", got) + if got := FindProjectGitRoot(leaf); got != "" { + t.Fatalf("FindProjectGitRoot = %q, want empty for invalid .git directory", got) } } diff --git a/internal/tui/tui_fixes_test.go b/internal/tui/tui_fixes_test.go index eb9043ce2..77e3f36ff 100644 --- a/internal/tui/tui_fixes_test.go +++ b/internal/tui/tui_fixes_test.go @@ -160,3 +160,21 @@ func suggestionNames2(s []commandSuggestion) []string { } return out } + +func TestGitBranchInSubdirectory(t *testing.T) { + root := t.TempDir() + gitdir := filepath.Join(root, ".git") + if err := os.MkdirAll(gitdir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(gitdir, "HEAD"), []byte("ref: refs/heads/feature-tui-branch\n"), 0o644); err != nil { + t.Fatal(err) + } + subdir := filepath.Join(root, "nested", "sub", "dir") + if err := os.MkdirAll(subdir, 0o755); err != nil { + t.Fatal(err) + } + if got := gitBranch(subdir); got != "feature-tui-branch" { + t.Fatalf("gitBranch in subdirectory = %q, want feature-tui-branch", got) + } +} diff --git a/internal/tui/view.go b/internal/tui/view.go index 497107662..e0a68246f 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -462,10 +462,15 @@ func shortenPath(path string) string { // both regular checkouts (.git dir) and worktrees (.git file). Returns "" on any // problem — the header simply omits the segment. func gitBranch(cwd string) string { - if strings.TrimSpace(cwd) == "" { + cwd = strings.TrimSpace(cwd) + if cwd == "" { return "" } - gitPath := filepath.Join(cwd, ".git") + gitRoot := agent.FindProjectGitRoot(cwd) + if gitRoot == "" { + return "" + } + gitPath := filepath.Join(gitRoot, ".git") info, err := os.Stat(gitPath) if err != nil { return "" @@ -481,6 +486,12 @@ func gitBranch(cwd string) string { if dir == "" { return "" } + // In worktree mode the gitdir is often RELATIVE (e.g. + // "gitdir: ../.git/worktrees/") — resolve it against the worktree root (gitRoot), not the + // process working directory, or HEAD lookup fails and we drop the branch. + if !filepath.IsAbs(dir) { + dir = filepath.Join(gitRoot, dir) + } headPath = filepath.Join(dir, "HEAD") }