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

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 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.

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. 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 (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.

// 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) 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.

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 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.

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →