# How the Sidebar Project Selector Resolves Git Worktrees to Main Repository Roots

> Learn how the sidebar project selector resolves Git worktrees to main repository roots using git rev-parse for efficient project management.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-07-25

---

**The sidebar project selector collapses multiple Git worktrees into a single project entry by resolving each working directory to a canonical `projectRoot` using `git rev-parse --git-common-dir`, then deduplicating sessions based on that root.**

The pi-web sidebar treats linked Git worktrees as part of their main repository rather than separate projects. This consolidation relies on a resolution pipeline that maps any working directory to its repository's root, ensuring users see one project row regardless of which worktree checkout they are currently using.

## Resolving Worktrees to Repository Roots

The resolution process begins in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) with the `resolveProject` function. This utility executes `git rev-parse --git-common-dir` and related commands to discover the shared `.git` directory, the worktree's top-level path, and the current branch.

For **linked worktrees**, the `projectRoot` is determined as the **parent directory of the shared `.git` directory** rather than the current working directory. This ensures that `/repo/main`, `/repo/feature-branch`, and `/repo/hotfix` all resolve to the same canonical root. The function caches results on `globalThis.__piProjectCache` for 60 seconds to avoid repeated shell executions.

```typescript
import { resolveProject } from '@/lib/worktree';

async function getRepoRoot(cwd: string) {
  const info = await resolveProject(cwd);
  console.log('Project root:', info.projectRoot);
  console.log('Current branch:', info.branch);
  console.log('Is linked worktree?', info.isWorktree);
}

```

## Fetching Worktree Metadata via API

When the selected folder changes, the `SessionSidebar` component fetches metadata from the `/api/worktrees` endpoint defined in [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts). This API returns the `projectRoot`, `isGit` status, `isTopLevel` flag, and a complete list of worktrees belonging to that repository.

The response is stored in the component's `worktreeState`, providing the freshest data for root resolution. This API layer separates the Git discovery logic from the UI, allowing the sidebar to react to directory changes without blocking the main thread.

## Deriving the Effective Project Root

The `projectRootFor` helper function in [`components/SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx) (lines 72-81) implements the fallback logic for determining which project root a given path belongs to. It first checks if the provided `cwd` matches the `worktreeState.forCwd` property for a fast-path return of `worktreeState.projectRoot`.

If no fresh state exists, it falls back to scanning `allSessions` to find any session matching the current working directory. This guarantees that **all worktrees in the same repository map to the same `projectRoot`**, regardless of whether the user is at the root of a worktree or nested deep within the directory structure.

```tsx
// Inside SessionSidebar.tsx
const projectRootFor = useCallback(
  (cwd: string | null): string | null => {
    if (!cwd) return null;
    // Fast path – use the most recent worktreeState
    if (worktreeState && worktreeState.forCwd === cwd) return worktreeState.projectRoot;
    // Fallback – look up any session that matches the cwd
    const match = allSessions.find(s => s.cwd === cwd);
    return match?.projectRoot ?? cwd;
  },
  [worktreeState, allSessions],
);

```

## Grouping Sessions and Deduplicating Projects

The `getRecentProjects` function (lines 94-106 in [`components/SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx)) builds a `Map<projectRoot, latestModified>` from all available sessions. This deduplication step ensures that multiple sessions opened from different worktrees of the same repository appear as a single row in the sidebar.

By grouping on `projectRoot` rather than the raw `cwd`, the UI maintains a clean, consolidated view where users interact with repositories as logical units rather than individual checkouts.

```typescript
function sessionsByProject(sessions: SessionInfo[]) {
  const groups = new Map<string, SessionInfo[]>();
  for (const s of sessions) {
    const root = s.projectRoot ?? s.cwd;
    if (!groups.has(root)) groups.set(root, []);
    groups.get(root)!.push(s);
  }
  return groups;
}

```

## Worktree Switching UI Logic

The sidebar renders the worktree dropdown only under specific conditions defined by `showWorktreeSwitcher` (lines 126-132). The selector appears when `worktreeState.isGit && worktreeState.isTopLevel && selectedProject === worktreeState.projectRoot`. This constraint prevents subdirectory sessions from displaying the worktree switcher, avoiding confusing project jumps when users navigate deep within a repository.

When a user clicks a different worktree entry, `handleSelectSessionFromList` calls `setSelectedCwd(wt.path)`. Because `projectRootFor(selectedCwd)` still resolves to the same canonical `projectRoot`, the session list remains grouped under the same project row, eliminating UI flicker and maintaining consistent project identity.

## Summary

- **`resolveProject`** in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) uses Git commands to find the shared `.git` directory and determine the canonical `projectRoot` for any path.
- **Caching** on `globalThis.__piProjectCache` prevents redundant shell executions for 60 seconds.
- **`projectRootFor`** implements a fast-path check against fresh API data with a fallback to session history, ensuring consistent root resolution.
- **Deduplication** via `getRecentProjects` groups all worktree sessions under a single project entry in the sidebar.
- **Conditional rendering** of the worktree switcher ensures it only appears at repository top-levels, preventing navigation confusion in subdirectories.

## Frequently Asked Questions

### How does the sidebar distinguish between a regular repository and a linked worktree?

The `resolveProject` function checks Git's metadata to determine if the current directory is a linked worktree. For linked worktrees, it sets `projectRoot` to the parent of the common `.git` directory rather than the current working directory, ensuring all worktrees resolve to the same repository root as implemented in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts).

### What happens if I open a session from a subdirectory inside a worktree?

The `projectRootFor` helper falls back to the session list lookup when the `worktreeState` does not match the current path. It finds the session entry for that specific `cwd` and returns its associated `projectRoot`, ensuring subdirectory sessions still map to the correct repository root and appear under the consolidated project row.

### How is the project resolution cached for performance?

Results from `resolveProject` are stored on `globalThis.__piProjectCache` with a 60-second TTL. This global cache prevents repeated execution of `git rev-parse` commands when the sidebar re-renders or when multiple components request resolution for the same path within a short timeframe.

### Can I programmatically resolve a path to its repository root using pi-web's utilities?

Yes, you can import `resolveProject` from `@/lib/worktree` to resolve any directory to its repository metadata. The function returns a `ProjectInfo` object containing `projectRoot`, `branch`, `isWorktree`, and other Git metadata, allowing you to build tools that understand repository boundaries the same way the sidebar does.