# Git Worktree Resolution and Project Grouping in pi-web: How It Works

> Learn how pi-web resolves git worktrees and groups projects. Discover how it unifies multiple checkouts under one UI using session grouping and worktree resolution.

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

---

**pi-web treats each session's current working directory as a potential Git repository, resolving linked worktrees via `git rev-parse` calls in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) and grouping sessions by project root in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) to unify multiple checkouts under a single UI node.**

pi-web is a web-based terminal session manager that requires precise Git repository context to organize multiple terminal sessions effectively. The system implements sophisticated **git worktree resolution** to detect whether a session's working directory belongs to a linked worktree or the main repository checkout, enabling intelligent project grouping in the session sidebar.

## Core Worktree Resolution Logic

All Git detection logic resides in **[`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts)**, which exports the primary `resolveProject` function used throughout the application to classify directory contexts and extract repository metadata.

### The resolveProject Function

The `resolveProject(cwd)` function (lines [76-110](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts#L76-L110)) executes four parallel `git rev-parse` commands to gather repository metadata:

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

const cwd = '/home/user/repo-feature';
const info = await resolveProject(cwd);
console.log(info);
// {
//   projectRoot: '/home/user/repo',
//   branch: 'feature',
//   isWorktree: true,
//   isTopLevel: true
// }

```

The function captures:
- **`commonDir`**: Path to the shared `.git` directory using `--git-common-dir`
- **`gitDir`**: The worktree-specific `.git` path via `--git-dir`
- **`toplevel`**: Absolute path of the checkout root via `--show-toplevel`
- **`ref`**: Current branch name via `--abbrev-ref HEAD`

### Detecting Linked Worktrees vs Top-Level Checkouts

After normalizing the current working directory with `realpathSync`, the system classifies the location through two boolean checks:

- **Top-level checkout**: Determined when `toplevel === realCwd`
- **Linked worktree**: Identified when `gitDir !== commonDir && isTopLevel`

For linked worktrees, the **`projectRoot`** becomes the parent directory of `commonDir` (the main repository root), while standard checkouts use the cwd itself. This distinction allows pi-web to treat all worktrees of the same repository as belonging to a single project.

### Handling Removed Worktrees

When a session points to a directory that no longer exists under the `<repoRoot>-worktrees` convention, the `inferRemovedWorktree` function (lines [68-74](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts#L68-L74)) fabricates a `ProjectInfo` object. This fallback mechanism folds the orphaned session back under the main repository root, preventing UI breakage while maintaining project associations.

### Caching Strategy

To avoid spawning repeated Git processes, pi-web caches resolution results on `globalThis.__piProjectCache` for 60 seconds (lines [40-47](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts#L40-L47)). This TTL-based cache ensures rapid session list refreshes without overwhelming the system with redundant `rev-parse` calls.

## Project Grouping for the Session Sidebar

The UI groups terminal sessions by repository using logic in **[`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)**, which bridges the worktree detection with the frontend display.

### Session Resolution and Metadata Attachment

The `loadAllSessions()` function (lines [21-28](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts#L21-L28)) iterates through all active sessions, calling `resolveProject` for each unique cwd and storing results in a `projectByCwd` map. When constructing each `SessionInfo` object (lines [30-44](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts#L30-L44)), the system:

1. Sets `projectRoot` to `project?.projectRoot ?? s.cwd`
2. Attaches `worktreeBranch` when the cwd represents a linked worktree

This metadata enables the sidebar to display sessions under a unified project tree node regardless of whether they originate from the main checkout or any linked worktree.

### Grouping Implementation

Sessions sharing the same `projectRoot`—whether from the main repository or linked worktrees—appear grouped together in the UI. Subdirectories inside a repository maintain their own identity with `isTopLevel === false`, ensuring accurate context preservation.

```typescript
// Session grouping – obtaining the list that the UI renders
import { listAllSessions } from '@/lib/session-reader';

const sessions = await listAllSessions();
const byProject = new Map<string, typeof sessions>();
for (const s of sessions) {
  const key = s.projectRoot;
  if (!byProject.has(key)) byProject.set(key, []);
  byProject.get(key)!.push(s);
}
console.table([...byProject.entries()].map(([proj, list]) => ({
  projectRoot: proj,
  sessionCount: list.length,
  worktreeBranches: [...new Set(list.map(x => x.worktreeBranch).filter(Boolean))]
})));

```

## API Endpoints for Worktree Management

The **[`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts)** file exposes HTTP endpoints that mirror the internal resolution logic, secured by path validation (lines [8-14](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts#L8-L14)).

### Listing and Resolving Worktrees

The **GET `/api/worktrees?cwd=`** endpoint (lines [26-44](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts#L26-L44)) returns a JSON object containing `projectRoot`, `isGit`, `isTopLevel`, and an array of available worktrees. This powers the "Switch worktree" UI component.

### Creating and Removing Worktrees

- **POST `/api/worktrees`**: Creates new linked worktrees via `addWorktree(cwd, branch)` (lines [66-68](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts#L66-L68))
- **DELETE `/api/worktrees`**: Removes worktrees safely using `removeWorktree` (lines [75-89](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts#L75-L89))

```typescript
// Add a new worktree via the backend API (client-side)
await fetch('/api/worktrees', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ cwd: '/home/user/repo', branch: 'new-branch' })
}).then(r => r.json()).then(console.log);
// → { path: '/home/user/repo-worktrees/new-branch', branch: 'new-branch' }

```

### Listing Available Worktrees

The `listWorktrees` function imported from [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) enumerates all worktrees for a given project root, distinguishing the main checkout from linked worktrees:

```typescript
// List all worktrees for a project (used by the sidebar switcher)
import { listWorktrees } from '@/lib/worktree';

const worktrees = await listWorktrees('/home/user/repo');
worktrees.forEach(w => {
  console.log(`${w.isMain ? '[main]' : '[worktree]'} ${w.path} → ${w.branch}`);
});

```

## Summary

- **pi-web** implements **git worktree resolution** in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) using `git rev-parse` with `--git-common-dir` and `--git-dir` flags to distinguish linked worktrees from main checkouts.
- The `resolveProject` function returns a `ProjectInfo` object containing `projectRoot`, `branch`, `isWorktree`, and `isTopLevel` flags, cached for 60 seconds to optimize performance.
- **Project grouping** occurs in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts), where sessions sharing the same `projectRoot` are aggregated under a single UI node regardless of their specific checkout path.
- The REST API in [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) exposes worktree management with path validation, supporting creation, deletion, and resolution operations.
- Removed worktrees are handled gracefully via `inferRemovedWorktree`, ensuring sessions always maintain valid project associations.

## Frequently Asked Questions

### How does pi-web detect if a directory is a linked Git worktree?

pi-web detects linked worktrees by comparing the `gitDir` (worktree-specific Git directory) with the `commonDir` (shared `.git` directory). When `gitDir !== commonDir` and the path is a top-level checkout, the system classifies it as a linked worktree and sets `isWorktree: true` in the `ProjectInfo` object, as implemented in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts).

### What happens to sessions when a worktree directory is deleted?

When a worktree directory no longer exists, the `inferRemovedWorktree` function (lines 68-74 in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts)) fabricates a fallback `ProjectInfo` object that associates the session with the main repository root. This prevents UI errors while maintaining the session's project grouping context.

### How does pi-web group sessions from different worktrees of the same repository?

The system groups sessions by `projectRoot`, which for linked worktrees resolves to the main repository root (the parent of the common Git directory). The `loadAllSessions` function in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) attaches this `projectRoot` to each session, causing the UI to render all worktrees of the same repository under a single collapsible tree node.

### Can pi-web create new Git worktrees through its API?

Yes, the **POST `/api/worktrees`** endpoint accepts a `cwd` and `branch` parameter, calling `addWorktree(cwd, branch)` from [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) to create a new linked worktree. The endpoint validates that the requested path is within an allowed file root before executing Git commands.