# How SessionSidebar Organizes Sessions by Project with Worktree Grouping in Pi-Web

> Learn how SessionSidebar organizes Pi sessions by project using worktree grouping in Pi-Web. Navigate seamlessly between worktrees while preserving session hierarchy for efficient project management.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-07-31

---

**The SessionSidebar component aggregates Pi sessions by their repository root (project) and collapses all worktrees into a unified view, allowing seamless navigation between worktrees while preserving the session hierarchy.**

The pi-web interface provides a unified sidebar that groups terminal sessions by their underlying Git project rather than individual working directories. This architecture allows developers to see all sessions across different worktrees of the same repository in one continuous tree. Understanding how `SessionSidebar` organizes sessions by project with worktree grouping reveals why the session hierarchy remains stable when switching between branches.

## Resolving Project Roots from Session Metadata

Every session carries metadata that identifies its project affiliation. The `SessionInfo` type defined in [`lib/types.ts`](https://github.com/agegr/pi-web/blob/main/lib/types.ts) includes an optional `projectRoot` property that points to the main repository directory, falling back to the session's `cwd` (current working directory) when unavailable.

In [`components/SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx), the `projectRootFor()` callback (lines 75-84) resolves the effective project root for any given path. It first checks if the cwd matches the cached `worktreeState`, then searches through `allSessions` to find a matching `projectRoot`. If no match exists, it returns the cwd itself as the project identifier.

```tsx
const projectRootFor = useCallback((cwd: string | null): string | null => {
  if (!cwd) return null;
  if (worktreeState && worktreeState.forCwd === cwd) return worktreeState.projectRoot;
  if (worktreeState?.worktrees.some((w) => w.path === cwd)) return worktreeState.projectRoot;
  const match = allSessions.find((s) => s.cwd === cwd);
  return match?.projectRoot ?? cwd;
}, [worktreeState, allSessions]);

```

## Prioritizing Active Projects with Recent Sorting

The sidebar surfaces the most relevant projects first using the `getRecentProjects()` function (lines 55-68). This utility builds a map from each project root to its most recently modified session timestamp, then sorts the roots in descending chronological order.

This approach ensures that active repositories appear at the top of the project dropdown, while stale projects sink to the bottom. The function extracts the root from either `s.projectRoot` or `s.cwd`, compares `modified` timestamps, and returns a sorted array of unique project paths.

```tsx
function getRecentProjects(sessions: SessionInfo[]): string[] {
  const latestByRoot = new Map<string, string>();
  for (const s of sessions) {
    const root = s.projectRoot ?? s.cwd;
    if (!root) continue;
    const prev = latestByRoot.get(root);
    if (!prev || s.modified > prev) latestByRoot.set(root, s.modified);
  }
  return [...latestByRoot.entries()]
    .sort((a, b) => b[1].localeCompare(a[1]))
    .map(([root]) => root);
}

```

## Aggregating Sessions Across All Worktrees

Once a user selects a project, the sidebar filters the global session list to show only entries belonging to that repository root. The `filteredSessions` array (lines 225-229) includes every session whose `projectRoot` (or fallback `cwd`) matches the `selectedProject`, regardless of which specific worktree spawned the session.

This aggregation happens at the project level rather than the directory level, ensuring continuity when developers switch between worktrees. The `buildSessionTree()` function (lines 54-96) then processes these filtered sessions to create the hierarchical parent-child view, sorting each level by modification date.

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

```

## Implementing the Worktree Switcher Interface

The sidebar fetches worktree metadata via the `/api/worktrees?cwd=...` endpoint defined in [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) when a project is selected. This returns the repository root, Git status, and an array of worktree entries stored in `worktreeState` (lines 108-132).

The UI renders a worktree switcher only when the current selection represents a top-level Git checkout belonging to the active project. The boolean `showWorktreeSwitcher` (lines 208-215) evaluates `worktreeState.isGit`, `worktreeState.isTopLevel`, and confirms that `selectedProject === worktreeState.projectRoot`.

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

```

## Preserving Session Hierarchy During Worktree Navigation

Switching worktrees updates the effective `cwd` via `setSelectedCwd` without rebuilding the `filteredSessions` array. Because the session list is already filtered to the project level, changing worktrees merely shifts the context while maintaining the existing tree structure (lines 226-230).

This design prevents UI flicker and keeps the session tree mounted during transitions. The sidebar logic in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) provides server-side helpers for resolving worktree metadata, ensuring the client-side state remains synchronized with the repository structure.

## Summary

- **Project root resolution**: The `projectRootFor()` helper in [`components/SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx) deduplicates sessions by checking `projectRoot` metadata or falling back to `cwd`.
- **Recent project sorting**: `getRecentProjects()` ranks repositories by their latest session activity to surface active work first.
- **Cross-worktree filtering**: `filteredSessions` aggregates all sessions matching the selected project root, ignoring specific worktree paths.
- **Conditional switcher**: The worktree dropdown appears only for top-level Git checkouts where `selectedProject === worktreeState.projectRoot`.
- **Stable hierarchy**: Session trees persist during worktree switches because filtering happens at the project level, not the directory level.

## Frequently Asked Questions

### How does SessionSidebar determine which project a session belongs to?

The component checks the `projectRoot` property on each `SessionInfo` object first. If that property is undefined, it falls back to the session's `cwd` (current working directory). The `projectRootFor()` utility in [`components/SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx) also considers cached worktree data to resolve the correct repository root for any given path.

### Why do sessions from different worktrees appear together in the sidebar?

The `filteredSessions` array is built by comparing each session's project root against the selected project, not by matching exact working directories. This means sessions from `/repo/main`, `/repo/feature-worktree`, and `/repo/hotfix-worktree` all appear under the same project grouping because they share the same repository root according to the logic in [`components/SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx).

### When does the worktree switcher dropdown appear?

The switcher renders only when four conditions align: the current path is a Git repository (`worktreeState?.isGit`), it represents the top-level checkout (`isTopLevel`), a valid `selectedCwd` exists, and the selected project matches the worktree's project root (`selectedProject === worktreeState.projectRoot`). This prevents the dropdown from appearing in non-Git directories or nested repositories.

### Does switching worktrees reload the session list?

No, the session list remains stable because filtering occurs at the project level before the worktree switcher engages. Changing the worktree updates `selectedCwd` and fetches fresh worktree metadata from `/api/worktrees`, but `filteredSessions` continues to show all project sessions. This architecture prevents tree collapse and UI flicker during branch switches.