# How Pi Web Detects and Switches Git Worktrees from the Sidebar

> Learn how Pi Web uses its backend API to detect and switch Git worktrees directly from the sidebar. Manage your projects efficiently with this intuitive feature.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: how-to-guide
- Published: 2026-08-14

---

**Pi Web detects git worktrees via a backend API that runs `git worktree list`, registers each worktree as an allowed file root, and exposes them to a React sidebar component where selecting a different worktree updates the active working directory.**

Pi Web is an open-source project management interface that treats git worktrees as first-class navigable contexts. Understanding how Pi Web detects and switches git worktrees from the sidebar reveals a clean separation between server-side git operations and client-side state management.

## The Three-Layer Architecture

Pi Web's worktree system operates across three coordinated layers: a **backend API** that interfaces with git, **client-side state** that caches worktree metadata, and a **sidebar UI** that renders the selector and handles user interaction.

### Backend API: Detecting and Registering Worktrees

The detection logic lives entirely on the server. In [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts), the `GET` handler validates permissions, resolves the project root, enumerates worktrees, identifies which one contains the current working directory, and registers every discovered worktree as an allowed file root.

```ts
// app/api/worktrees/route.ts – GET handler
export async function GET(req: Request) {
  const cwd = new URL(req.url).searchParams.get("cwd");
  // … permission checks omitted …
  const project = await resolveProject(cwd);
  const worktrees = await listWorktrees(existsSync(cwd) ? cwd : project.projectRoot);
  const currentWorktreePath = findCurrentWorktreePath(worktrees, cwd);
  // Register each worktree so the file explorer can browse it
  for (const w of worktrees) allowFileRoot(w.path);
  return NextResponse.json({
    projectRoot: project.projectRoot,
    isGit: true,
    isTopLevel: project.isTopLevel,
    currentWorktreePath,
    worktrees,
  });
}

```

The `listWorktrees` function (from [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts)) executes `git worktree list --json` to obtain path, branch, and main-worktree status for each entry. `findCurrentWorktreePath` then matches the incoming `cwd` against these paths to determine `currentWorktreePath`. Critically, the loop calling `allowFileRoot(w.path)` pre-authorizes every worktree directory, eliminating permission checks when the user later switches between them.

## Client-Side State: Caching Worktree Data

The sidebar maintains **worktreeState**—a client-side mirror of the API payload. This state refreshes automatically whenever `selectedCwd` changes.

```tsx
// components/SessionSidebar.tsx – useLayoutEffect that loads worktrees
useLayoutEffect(() => {
  if (!selectedCwd) { setWorktreeState(null); return; }
  fetch(`/api/worktrees?cwd=${encodeURIComponent(selectedCwd)}`)
    .then(r => r.json())
    .then(d => {
      if (d.error || !d.projectRoot) { setWorktreeState(null); return; }
      setWorktreeState({
        forCwd: selectedCwd,
        projectRoot: d.projectRoot,
        isGit: d.isGit ?? false,
        isTopLevel: d.isTopLevel ?? false,
        currentWorktreePath: d.currentWorktreePath ?? null,
        worktrees: d.worktrees ?? [],
      });
    })
    .catch(() => setWorktreeState(null));
}, [selectedCwd, wtRefreshKey, refreshKey]);

```

The dependency array `[selectedCwd, wtRefreshKey, refreshKey]` ensures the worktree list reloads on three triggers: directory changes, manual refresh requests, and worktree creation/deletion operations.

## Sidebar UI: Rendering and Switching Worktrees

The selector component maps `worktreeState.worktrees` to dropdown options, highlights the active entry, and calls `setSelectedCwd` on change.

```tsx
// Inside SessionSidebar.tsx – worktree selector (simplified)
{worktreeState && (
  <select
    value={worktreeState.currentWorktreePath ?? worktreeState.forCwd}
    onChange={e => setSelectedCwd(e.target.value)}
  >
    {worktreeState.worktrees.map(w => (
      <option key={w.path} value={w.path}>
        {w.branch} ({w.isMain ? "main" : "worktree"})
      </option>
    ))}
  </select>
)}

```

Switching worktrees requires no additional API calls because the target path was already registered via `allowFileRoot`. The file explorer immediately displays the new worktree's contents. The `setWtRefreshKey` mechanism (triggered by `POST` or `DELETE` to `/api/worktrees`) forces a re-fetch after structural changes to keep the UI synchronized with git's actual state.

## Key Implementation Files

| File | Responsibility |
|------|-------------|
| [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) | HTTP endpoint for worktree enumeration and registration |
| [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) | Git command wrappers: `listWorktrees`, `findCurrentWorktreePath`, `addWorktree`, `removeWorktree`, `resolveProject` |
| [`components/SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx) | React component managing worktree state and rendering the selector |
| [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) | File-system security boundary: `allowFileRoot`, `isFilePathAllowed` |
| [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) | Session context providing `selectedCwd` and change notifications |

## Summary

- **Detection** occurs server-side via `git worktree list` in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts), with `findCurrentWorktreePath` matching the current directory to a worktree entry
- **Registration** of all worktrees via `allowFileRoot` enables instant client-side switching without permission rechecks
- **State management** in [`SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/SessionSidebar.tsx) couples `worktreeState` to `selectedCwd` through a `useLayoutEffect` fetch
- **Switching** simply updates `selectedCwd`, triggering cascading UI updates for the session list and file explorer

## Frequently Asked Questions

### How does Pi Web determine which worktree is currently active?

Pi Web's `findCurrentWorktreePath` function in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) iterates through the output of `git worktree list` and returns the first path that contains or equals the requested `cwd`. This path becomes `currentWorktreePath` in the API response, which the sidebar uses to highlight the active selection.

### Why does the API register all worktrees instead of just the current one?

The `allowFileRoot` loop in [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) pre-authorizes every discovered worktree so that switching between them requires no additional permission validation. This design eliminates latency and complexity in the UI—once the initial load completes, all worktrees are immediately browsable.

### What happens when I create or delete a worktree outside Pi Web?

The sidebar may become temporarily out of sync. Pi Web provides a manual refresh mechanism via `wtRefreshKey`—mutation endpoints (`POST` or `DELETE /api/worktrees`) increment this key, which appears in the `useLayoutEffect` dependency array and triggers a fresh fetch of `git worktree list`.