How the Pi‑Web Project Sidebar Implements Worktree‑Aware Grouping

The Pi‑Web sidebar treats every Git worktree that belongs to the same repository as a single project, grouping sessions from the main checkout and linked worktrees under one unified view.

In the agegr/pi-web repository, the sidebar component (components/SessionSidebar.tsx) solves the fragmentation problem that occurs when developers use multiple Git worktrees. Instead of scattering sessions across isolated directory entries, the UI automatically detects when directories belong to the same repository and aggregates them into a single project node. This worktree-aware grouping relies on repository‑root resolution, cached worktree metadata, and hierarchical session trees.

Resolving the Project Root from Any Directory

The sidebar must determine which project a session belongs to before it can group items. Because a user might open a file inside a linked worktree rather than the main repository, the component uses a resolution strategy that prioritizes Git metadata over simple path matching.

The projectRootFor Helper

At the core of this logic is the projectRootFor callback defined around lines 73‑80 in components/SessionSidebar.tsx. This function accepts a cwd (current working directory) and returns the canonical project root:

const projectRootFor = useCallback(
  (cwd: string | null) => {
    if (!cwd) return null;
    // Prefer cached worktree data
    if (worktreeState && worktreeState.forCwd === cwd) 
      return worktreeState.projectRoot;
    // Fallback: any session that matches this cwd
    const match = allSessions.find((s) => s.cwd === cwd);
    return match?.projectRoot ?? cwd;
  },
  [worktreeState, allSessions]
);

The helper first checks worktreeState, a piece of state that stores the resolved projectRoot for the currently selected directory. If the cache misses, it scans allSessions for a session whose cwd matches, returning that session’s projectRoot or falling back to the original directory.

Fallback Session Matching

When no worktree cache exists—such as during initial load—the sidebar iterates through the session list to find any entry located in the target directory. This ensures that even before the async worktree discovery completes, the UI can make an educated guess about project affiliation based on previously recorded session metadata.

Filtering Sessions by Project Identity

Once the sidebar identifies the active project root, it filters the global session list to show only relevant items. The filtering logic at lines 224‑226 compares each session’s projectRoot (or its fallback cwd) against the resolved project:

const selectedProject = projectRootFor(selectedCwd);
const filteredSessions = selectedProject
  ? allSessions.filter((s) => (s.projectRoot ?? s.cwd) === selectedProject)
  : allSessions;

This comparison is strictly equality‑based; because all worktrees of the same repository share an identical projectRoot value (the main repository’s top‑level directory), sessions from /repo/main, /repo/feature-a, and /repo/hotfix-b naturally satisfy the same predicate and appear together.

Building the Hierarchical Session Tree

Raw session lists are flat, but the sidebar renders a collapsible tree. The component transforms the filtered array via buildSessionTree(), located approximately lines 93‑135 in components/SessionSidebar.tsx.

Parent‑Child Relationships

The function constructs a hierarchy by traversing parentSessionId chains. Sessions that spawn child processes (such as terminals created from within an existing editor session) nest underneath their logical parent rather than cluttering the top level.

Sorting by Modification Date

After tree construction, each level is sorted descending by the most recent modification timestamp. This keeps active sessions visible at the top of the project group while preserving the logical parent‑child structure.

Discovering Git Worktrees

Worktree awareness requires accurate Git metadata. When the selected directory changes, the sidebar fetches data from the /api/worktrees?cwd=... endpoint, implemented in the lib/worktree.ts module.

Listing Linked Worktrees

The listWorktrees function (lines 32‑66) executes git worktree list --porcelain and parses the output into structured WorktreeInfo objects:

export async function listWorktrees(cwd: string): Promise<WorktreeInfo[]> {
  const out = await git(cwd, ["worktree", "list", "--porcelain"]);
  const worktrees: WorktreeInfo[] = [];
  let current: (Partial<WorktreeInfo> & { prunable?: boolean }) | null = null;
  // …parse porcelain format and push non‑prunable entries…
  return worktrees;
}

This list includes paths to linked worktrees, their branch refs, and whether they are prunable (bare checkouts), allowing the UI to present valid switch targets.

Resolving Repository Metadata

To determine whether the current directory sits inside a worktree versus the main checkout, lib/worktree.ts exposes resolveProject. It runs git rev-parse --git-common-dir --git-dir --show-toplevel and compares paths:

export async function resolveProject(cwd: string): Promise<ProjectInfo> {
  const out = await git(cwd, [
    "rev-parse", "--path-format=absolute",
    "--git-common-dir", "--git-dir", "--show-toplevel",
    "--abbrev-ref", "HEAD",
  ]);
  const [commonDir, gitDir, toplevel, ref] = out.split("\n");
  const isTopLevel = toplevel === realpathSync(cwd);
  const isWorktreeTopLevel = gitDir !== commonDir && isTopLevel;
  return {
    projectRoot: isWorktreeTopLevel ? dirname(commonDir) : cwd,
    branch: ref && ref !== "HEAD" ? ref : null,
    isWorktree: isWorktreeTopLevel,
    isTopLevel,
  };
}

If gitDir differs from commonDir, Git has placed the directory in a linked worktree, and the function returns the parent of the common Git directory as the canonical projectRoot.

Conditional Worktree Switcher UI

The sidebar renders a dropdown that lets users switch between the main checkout and any linked worktrees, but only when three conditions align. The showWorktreeSwitcher boolean (lines 226‑231) guards the render:

const showWorktreeSwitcher = Boolean(
  worktreeState?.isGit &&
  worktreeState?.isTopLevel &&
  selectedCwd &&
  selectedProject === worktreeState.projectRoot
);

The dropdown appears only when the current directory is a Git repository, sits at the worktree’s top level (not a subdirectory), and matches the resolved project root. This prevents the switcher from cluttering the UI when the user browses subfolders or non‑Git directories.

Synchronizing State Across Worktree Operations

Creating or removing worktrees requires immediate UI synchronization to maintain the grouping illusion.

Optimistic Creation

When a user triggers handleCreateWorktree, the component optimistically appends the new worktree path to worktreeState.worktrees (lines 630‑638). This allows projectRootFor() to resolve the new directory instantly without waiting for a network round‑trip, preventing flicker or "unrecognized project" states.

Removal Handling

If a worktree is deleted, the component increments a refresh key (setWtRefreshKey) to re‑fetch the worktree list. Should the removed worktree be the current cwd, the sidebar automatically switches selectedCwd back to the main repository root (lines 669‑670), ensuring the user never stares at a orphaned directory reference.

Summary

  • Single Project View: The sidebar groups sessions by projectRoot, ensuring all worktrees of the same repository appear under one heading.
  • Root Resolution: The projectRootFor helper prioritizes cached worktree data from lib/worktree.ts and falls back to session metadata.
  • Tree Construction: buildSessionTree organizes filtered sessions into parent‑child hierarchies sorted by modification time.
  • Conditional UI: The worktree switcher renders only at repository top‑levels when isGit, isTopLevel, and project alignment are satisfied.
  • State Sync: Optimistic updates and automatic fallback handling keep the sidebar responsive during worktree lifecycle events.

Frequently Asked Questions

What is Git worktree grouping in pi-web?

Git worktree grouping refers to the sidebar’s behavior of treating multiple linked worktrees (created via git worktree add) as a single project node. According to the agegr/pi-web source code, the SessionSidebar.tsx component achieves this by resolving every directory to its canonical projectRoot using git rev-parse --git-common-dir, then filtering sessions that share that root. This prevents the UI from fragmenting sessions across separate directory entries when they logically belong to the same codebase.

How does the sidebar determine the correct project root for a session?

The sidebar uses the projectRootFor callback in components/SessionSidebar.tsx. It first checks the cached worktreeState object populated by lib/worktree.ts; if the cache contains a match for the current cwd, it returns the stored projectRoot. Otherwise, it searches allSessions for any entry with a matching cwd and uses that entry’s projectRoot field, falling back to the raw directory path only when no metadata exists.

When does the worktree switcher dropdown appear?

The switcher appears only when three conditions evaluate to true: the current directory is inside a Git repository (worktreeState.isGit), the directory represents the top‑level of a checkout (worktreeState.isTopLevel), and the resolved selectedProject matches the cached projectRoot. This logic, implemented in the showWorktreeSwitcher constant around lines 226‑231, ensures the dropdown is contextually relevant and hidden when browsing subdirectories or non‑Git folders.

How are sessions organized within the project tree?

After filtering sessions to the current project, the sidebar calls buildSessionTree() to construct a hierarchy based on parentSessionId relationships. Sessions with no parent appear at the top level, while child sessions nest underneath their progenitors. Each level is then sorted by the session’s last modification timestamp, placing the most recently active items first according to the implementation around lines 93‑135 in components/SessionSidebar.tsx.

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 →