# How Pi-Web Handles In-Session Branch Navigation: A Technical Deep Dive

> Learn how Pi-Web handles in-session branch navigation using the navigate_tree command for efficient context switching without session file rewrites. Explore the technical details.

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

---

**Pi-Web implements in-session branch navigation by dispatching a `navigate_tree` command to the server, which updates the AgentSession's internal pointer to the target leaf and returns the corresponding conversation context without rewriting the underlying session file.**

The `agegr/pi-web` repository manages parallel conversation branches within a single session file using a lightweight pointer-based system. Unlike traditional forking mechanisms that duplicate files, **pi-web branch navigation** allows users to switch between alternative response paths instantly by manipulating the session state and retrieving the relevant message history on demand.

## The navigate_tree Command Architecture

When a user selects a different branch via the Continue button or BranchNavigator component, the system executes a coordinated sequence between the frontend and backend that preserves session integrity while updating the active conversation path.

### Triggering Navigation from the UI

The process begins in [`components/BranchNavigator.tsx`](https://github.com/agegr/pi-web/blob/main/components/BranchNavigator.tsx) when user interaction triggers a command dispatch. The UI calls `sendAgentCommand` from [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) (approximately line 1199) with a specific payload structure:

```typescript
import { sendAgentCommand } from '@/lib/agent-client';

function onBranchSelect(entryId: string, sessionId: string) {
  sendAgentCommand(sessionId, { 
    type: 'navigate_tree', 
    targetId: entryId 
  });
}

```

This function performs a **POST** request to `/api/agent/[sessionId]`, transmitting the `targetId` (the leaf entry ID) to the server for processing.

### Server-Side Command Processing

The request reaches [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), where the dispatcher handles the `navigate_tree` case (lines 430‑436). This implementation includes critical safety checks before executing the navigation:

```typescript
// lib/rpc-manager.ts (excerpt)
case "navigate_tree": {
  if (this.inner.isBashRunning) {
    throw new Error("Cannot navigate while a shell command is running");
  }
  const result = await this.inner.navigateTree(
    command.targetId as string, 
    {}
  );
  return { cancelled: result.cancelled };
}

```

The `this.inner` reference points to the **AgentSession** instance from the Pi SDK. Calling `navigateTree(targetId, {})` rewrites the session's internal pointer to the specified entry ID and returns a `{ cancelled: boolean }` status. If a Bash command is currently executing, the navigation aborts immediately to prevent session state corruption.

### Retrieving Branch Context

After successful navigation, the frontend requests the conversation context for the new branch. The `useAgentSession` hook calls **GET** `/api/sessions/[id]/context?leafId=` to retrieve the message history:

```typescript
// hooks/useAgentSession.ts (simplified)
async function fetchLeafContext(leafId: string, sessionId: string) {
  const res = await fetch(
    `/api/sessions/${sessionId}/context?leafId=${leafId}`
  );
  const { messages, entryIds } = await res.json();
  setMessages(messages);
  setEntryIds(entryIds);
}

```

The endpoint handler in `app/api/sessions/[id]/context/route.ts` reads the `.jsonl` session file and builds a `SessionContext` containing parallel arrays of `messages` and `entryIds`, returning only the messages that exist along the path to the requested leaf.

## Key Technical Concepts

Understanding pi-web branch navigation requires distinguishing between different branching mechanisms and understanding the underlying data structures.

### In-Session Branches vs. Forks

Pi-web implements two distinct branching strategies:

- **In-session branches**: Stay within the same `.jsonl` file. The only difference between branches is the `parentId` stored in leaf entries. Navigation moves an internal pointer without file I/O.
- **Forks**: Create a brand-new `.jsonl` file (visible as a child node in the sidebar), duplicating the session data to a separate persistence layer.

According to the [`AGENTS.md`](https://github.com/agegr/pi-web/blob/main/AGENTS.md) documentation section "Two kinds of branching," the in-session approach is optimized for rapid exploration of alternative responses, while forks are designed for long-term parallel development tracks.

### The entryIds Mapping

The `SessionContext` includes an `entryIds` array that maintains a strict one-to-one mapping with the `messages` array. Each index position in `entryIds` contains the corresponding `.jsonl` entry ID for that message. This mapping, built by [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts), enables the UI to pass the correct `targetId` back to the server when initiating navigation, ensuring the `navigateTree` method receives a valid session pointer.

### Safety and State Consistency

The navigation system includes protective mechanisms to prevent invalid state transitions. The `isBashRunning` check in [`rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/rpc-manager.ts) ensures that users cannot switch branches while a shell command is executing, as this would disconnect the running process from its conversation context and potentially leave orphaned background tasks.

## Implementation Reference

The following code examples demonstrate the complete flow from UI interaction to server response.

### BranchNavigator Component

Located in [`components/BranchNavigator.tsx`](https://github.com/agegr/pi-web/blob/main/components/BranchNavigator.tsx), this component handles user selection and initiates the command:

```typescript
import { useAgentSession } '@/hooks/useAgentSession';

function BranchNavigator({ sessionId }: { sessionId: string }) {
  const { sendAgentCommand } = useAgentSession(sessionId);
  
  const handleSelect = (entryId: string) => {
    sendAgentCommand({
      type: 'navigate_tree',
      targetId: entryId
    }).catch(() => {
      // UI remains on current branch if navigation fails
    });
  };
  
  return (
    // Branch selection UI implementation
  );
}

```

### Context Endpoint Handler

The server-side route in `app/api/sessions/[id]/context/route.ts` processes context requests:

```typescript
import { buildSessionContext } from '@/lib/session-reader';

export default async function handler(req, res) {
  const { id, leafId } = req.query;
  const sessionFile = await resolveSessionFile(id);
  const ctx = await buildSessionContext(sessionFile, { leafId });
  
  res.json({
    messages: ctx.messages,
    entryIds: ctx.entryIds,
    // Additional context metadata
  });
}

```

## Summary

- **Pi-web branch navigation** uses the `navigate_tree` command type to move between parallel conversation paths within a single session file.
- The **AgentSession** class from the Pi SDK maintains an internal pointer that changes when `navigateTree()` is called, requiring no file rewrite operations.
- **Safety checks** in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) prevent navigation during active Bash execution to maintain process consistency.
- The **`entryIds[]` array** in `SessionContext` creates a bidirectional mapping between UI messages and their underlying `.jsonl` entry identifiers.
- **Two branching modes** exist: in-session navigation (lightweight pointer movement) and forking (new file creation), documented in [`AGENTS.md`](https://github.com/agegr/pi-web/blob/main/AGENTS.md).

## Frequently Asked Questions

### What is the difference between in-session branch navigation and forking in pi-web?

In-session branch navigation keeps all conversation data within the original `.jsonl` file by changing the `parentId` relationships between entries and moving the session's internal pointer. Forking creates an entirely new `.jsonl` file on the filesystem, duplicating the conversation state to a separate persistence layer that appears as a child node in the sidebar.

### How does pi-web prevent navigation during active shell commands?

The [`rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/rpc-manager.ts) file checks `this.inner.isBashRunning` before executing any `navigate_tree` command. If a Bash process is active, the dispatcher throws an error and returns a failure response to the UI, preventing the session pointer from changing while a command is executing and avoiding orphaned processes or corrupted state.

### What role does the entryIds array play in branch navigation?

The `entryIds` array in `SessionContext` maps each displayed message to its corresponding entry ID in the `.jsonl` session file. When a user selects a branch, the UI passes the specific entry ID from this mapping as the `targetId` parameter in the `navigate_tree` command, ensuring the server identifies the correct leaf node for navigation.

### Why doesn't pi-web rewrite the session file when switching branches?

The navigation mechanism only updates the internal pointer of the `AgentSession` instance to reference a different entry ID in the existing file. Since the underlying `.jsonl` data remains unchanged (only the active position changes), no file rewrite is necessary, enabling instant branch switching without I/O overhead or data duplication.