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

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 and grouping sessions by project root in 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, 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) executes four parallel git rev-parse commands to gather repository metadata:

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) 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). 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, which bridges the worktree detection with the frontend display.

Session Resolution and Metadata Attachment

The loadAllSessions() function (lines 21-28) 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), 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.

// 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 file exposes HTTP endpoints that mirror the internal resolution logic, secured by path validation (lines 8-14).

Listing and Resolving Worktrees

The GET /api/worktrees?cwd= endpoint (lines 26-44) 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)
  • DELETE /api/worktrees: Removes worktrees safely using removeWorktree (lines 75-89)
// 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 enumerates all worktrees for a given project root, distinguishing the main checkout from linked worktrees:

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

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) 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 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 to create a new linked worktree. The endpoint validates that the requested path is within an allowed file root before executing Git commands.

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 →