How SessionSidebar Organizes Sessions by Project with Worktree Grouping in Pi-Web
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 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, 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.
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.
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.
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 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.
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 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 incomponents/SessionSidebar.tsxdeduplicates sessions by checkingprojectRootmetadata or falling back tocwd. - Recent project sorting:
getRecentProjects()ranks repositories by their latest session activity to surface active work first. - Cross-worktree filtering:
filteredSessionsaggregates 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 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.
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.
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 →