Pi Web Worktree Creation and Dirty Worktree Handling: A Complete Technical Guide

Pi Web manages Git worktrees by treating each checkout as a separate session while grouping them under a common project root, with explicit handling for dirty worktrees through API-level error detection and user-controlled force removal.

Pi Web's architecture enables developers to work on multiple branches simultaneously without losing session history. This guide examines how the agegr/pi-web repository implements worktree lifecycle management, from creation through safe removal of repositories with uncommitted changes.

How Pi Web Resolves Worktree Projects

The foundation of Pi Web's worktree management sits in lib/worktree.ts, where resolveProject() (line 85) determines the relationship between a session's current working directory and its underlying Git structure.

Project Resolution Properties

When resolveProject() analyzes a directory, it returns four critical properties:

Property Purpose
projectRoot Top-level directory containing the shared .git directory
branch Current Git branch, or null for detached HEAD
isWorktree true only for linked worktrees (not the main repository)
isTopLevel true when cwd equals worktree root; subdirectories become separate projects

The function executes git rev-parse --git-common-dir … --show-toplevel through the git() helper (lines 52-60), then normalizes paths using toNativePath and realPathOrSelf.

Performance is protected by a 60-second TTL cache on globalThis.__piProjectCache, preventing repeated Git invocations for the same directory.

Creating New Worktrees in Pi Web

The addWorktree(cwd, branch) function (lines 93-102) implements a six-step creation process:

  1. Sanitize branch name — sanitizeBranchForDir() converts branch names to safe directory names
  2. Locate repository root — getRepoRoot() finds the main Git directory
  3. Create target directory — mkdirSync(..., {recursive:true}) establishes <repoRoot>-worktrees/<sanitized-branch>
  4. Execute Git worktree add — either git worktree add -- <path> <branch> for existing branches, or git worktree add -b <branch> -- <path> to create from HEAD
  5. Register file access — allowFileRoot(worktreePath) enables Pi Web's file API to browse the new location
  6. Invalidate cache — clears the project cache to reflect the updated layout
// Client-side worktree creation
await fetch('/api/worktrees', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ cwd: '/home/user/project', branch: 'feature/awesome' })
});
// Response: { path: '/home/user/project-worktrees/feature-awesome', branch: 'feature/awesome' }

Listing and Managing Existing Worktrees

listWorktrees(cwd) (lines 44-78) parses git worktree list --porcelain into structured objects with path, branch, and isMain properties. The function filters out prunable or missing worktrees, ensuring the UI only displays usable entries.

Removing Worktrees and Detecting Dirty States

The removeWorktree(cwd, worktreePath, force) function (lines 32-44) implements safety-first deletion:

  1. Retrieves current worktree list and locates the target
  2. Blocks main worktree removal — target.isMain triggers an error
  3. Executes git worktree remove [--force] <path>
  4. Captures dirty errors — re-throws if Git reports uncommitted changes
  5. Clears project cache after successful removal

API-Level Dirty Worktree Detection

The HTTP route in app/api/worktrees/route.ts (lines 95-98) transforms Git errors into machine-readable responses:

// Detect dirty worktree errors from git
const dirty = /contains modified or untracked files|is dirty/i.test(message);
return NextResponse.json({ error: message, dirty }, { status: dirty ? 409 : 400 });

This design yields two distinct response patterns:

  • 409 Conflict with dirty: true — uncommitted or untracked files present
  • 400 Bad Request — other removal failures

The front-end in components/SessionSidebar.tsx (lines 792-796) consumes this flag, prompting users before executing force=true retries.

Complete Dirty Worktree Handling Flow

async function deleteWorktree(cwd: string, path: string) {
  const resp = await fetch(
    `/api/worktrees?cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(path)}`,
    { method: 'DELETE' }
  );
  const data = await resp.json();
  
  if (resp.status === 409 && data.dirty) {
    // Prompt user confirmation, then force removal
    await fetch(
      `/api/worktrees?force=true&cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(path)}`,
      { method: 'DELETE' }
    );
  }
}

Why Pi Web Protects Dirty Worktrees

Git's native safety mechanism refuses worktree deletion when local modifications would be lost. Pi Web preserves this protection while adding UX refinement:

  • Explicit user consent — the dirty flag enables informed force decisions
  • Data loss prevention — accidental deletions require deliberate opt-in
  • Abandoned worktree cleanup — force removal remains available for intentional cleanup

Summary

  • Project resolution: resolveProject() in lib/worktree.ts uses Git commands and 60-second caching to map directories to worktree metadata
  • Worktree creation: addWorktree() sanitizes branches, creates directories, registers file access, and handles both existing and new branches
  • Safe removal: removeWorktree() prevents main worktree deletion and surfaces dirty states to callers
  • API translation: app/api/worktrees/route.ts converts Git errors to HTTP 409 responses with structured dirty flags
  • User control: SessionSidebar.tsx interprets dirty flags to prompt for force removal, balancing safety with flexibility

Frequently Asked Questions

How does Pi Web prevent accidental deletion of worktrees with uncommitted changes?

Pi Web mirrors Git's native protection by detecting "dirty" errors in app/api/worktrees/route.ts using regex matching against Git's error messages. The API returns HTTP 409 with dirty: true, requiring the front-end to explicitly request force=true after user confirmation. This two-step process ensures intentional deletion of modified worktrees.

What directory structure does Pi Web use for worktrees?

Pi Web creates worktrees at <repoRoot>-worktrees/<sanitized-branch>, where sanitizeBranchForDir() converts branch names like feature/awesome to filesystem-safe strings. This convention keeps all related checkouts discoverable under a predictable sibling directory of the main repository.

Can Pi Web remove the main repository worktree?

No. The removeWorktree() function explicitly checks target.isMain and blocks deletion of the primary worktree. This safeguard prevents destruction of the repository's anchor point, which would orphan all linked worktrees and corrupt the shared .git directory structure.

How does Pi Web optimize worktree resolution performance?

A short-lived cache on globalThis.__piProjectCache with 60-second TTL prevents repeated Git command execution for the same directory. The cache is explicitly invalidated after worktree additions and removals to maintain consistency with the actual filesystem state.

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 →