How Pi Web Detects and Switches Git Worktrees from the Sidebar
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, 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.
// 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) 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.
// 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.
// 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 |
HTTP endpoint for worktree enumeration and registration |
lib/worktree.ts |
Git command wrappers: listWorktrees, findCurrentWorktreePath, addWorktree, removeWorktree, resolveProject |
components/SessionSidebar.tsx |
React component managing worktree state and rendering the selector |
lib/file-access.ts |
File-system security boundary: allowFileRoot, isFilePathAllowed |
hooks/useAgentSession.ts |
Session context providing selectedCwd and change notifications |
Summary
- Detection occurs server-side via
git worktree listinlib/worktree.ts, withfindCurrentWorktreePathmatching the current directory to a worktree entry - Registration of all worktrees via
allowFileRootenables instant client-side switching without permission rechecks - State management in
SessionSidebar.tsxcouplesworktreeStatetoselectedCwdthrough auseLayoutEffectfetch - 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 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →