# Fork vs In‑Session Branch in Pi Web: Key Differences Explained

> Understand the key differences between fork and in-session branches in Pi Web. Learn how forks create new files while in-session branches navigate existing data.

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

---

**Forks create new `.jsonl` session files on disk, while in‑session branches navigate between leaves within the same file without creating new storage.**

Pi Web, an open-source AI conversation interface from `agegr/pi-web`, offers two distinct mechanisms for exploring alternative conversation paths. Understanding the difference between **fork** and **in‑session branch** helps you choose the right workflow for managing multi-turn conversations.

## What a Fork Creates

A **fork** produces a completely new session file that exists alongside the original.

When you click the **Fork** button on a user message in [`MessageView.tsx`](https://github.com/agegr/pi-web/blob/main/MessageView.tsx), or call the `fork` RPC command, Pi Web invokes `SessionManager.createBranchedSession` in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts). This operation copies conversation history up to the fork point into a fresh `.jsonl` file.

```ts
// lib/rpc-manager.ts – fork handling
// https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts#L399-L432
case "fork": {
  if (this.inner.isBashRunning) {
    throw new Error("Cannot fork while a shell command is running");
  }
  const entryId = command.entryId as string;
  // … validation logic …
  if (!entry.parentId) {
    // Fork before the first message: empty session
    const newManager = SessionManager.create(sessionManager.getCwd(), sessionDir);
    newManager.newSession({ parentSession: currentSessionFile });
    newSessionFile = newManager.getSessionFile() as string;
  } else {
    // Fork after some history: copy up to fork point
    const sourceManager = SessionManager.open(currentSessionFile, sessionDir);
    const forkedPath = sourceManager.createBranchedSession(entry.parentId);
    if (!forkedPath) throw new Error("Failed to create forked session");
    newSessionFile = forkedPath;
  }
  // …
  this.destroy();               // unload the old wrapper
  return { cancelled: false, newSessionId };
}

```

Critical behaviors of forks:

- **New file on disk**: Each fork gets its own `.jsonl` storage
- **Parent pointer**: The `parentSession` header links back to the original
- **Wrapper destruction**: [`rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/rpc-manager.ts) calls `this.destroy()` to unload the current session before loading the new one
- **Sidebar visibility**: Forks appear as **child nodes** under the original session in [`SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/SessionSidebar.tsx), with expand/collapse controls

## What an In‑Session Branch Does

An **in‑session branch** navigates between **leaf nodes** inside the *same* session file.

When you select a leaf in the **BranchNavigator** UI, or call the `navigate_tree` RPC command, Pi Web updates the session's active leaf pointer without touching disk storage.

```ts
// lib/rpc-manager.ts – navigate_tree handling
// https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts#L36-L42
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 };
}

```

Critical behaviors of in‑session branches:

- **No new file**: The `.jsonl` remains unchanged; only context shifts
- **Leaf ID targeting**: `inner.navigateTree(targetId, {})` moves the active pointer
- **Memory-only operation**: Faster than fork since no file I/O occurs
- **UI representation**: Shown as selectable **leaves** in [`BranchNavigator.tsx`](https://github.com/agegr/pi-web/blob/main/BranchNavigator.tsx) with path highlighting

## Side-by-Side Comparison

| Aspect | Fork | In‑Session Branch |
|--------|------|-------------------|
| **Storage** | New `.jsonl` file created | Same file, new leaf pointer |
| **Trigger** | `fork` RPC or UI button | `navigate_tree` RPC or leaf selection |
| **Backend method** | `createBranchedSession` then `destroy()` | `navigateTree(targetId, {})` |
| **Parent relationship** | `parentSession` header stored | None (same file) |
| **UI location** | [`SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/SessionSidebar.tsx) child nodes | [`BranchNavigator.tsx`](https://github.com/agegr/pi-web/blob/main/BranchNavigator.tsx) dropdown |
| **Performance** | Slower (file creation + copy) | Faster (in-memory pointer update) |
| **Use case** | Long-term alternative experiments | Quick exploration within a session |

## How to Trigger Each Mechanism

### Programmatic Fork

```ts
// Fork at a specific message entry
await sessionRpc.send({ type: "fork", entryId: "8a3f2c1e" });

```

### Programmatic Branch Navigation

```ts
// Switch to a different leaf in the same session
await sessionRpc.send({ type: "navigate_tree", targetId: "leaf-42" });

```

### UI Workflows

1. **Fork**: Hover over any user message → click the **Fork** icon → new child session appears in sidebar
2. **In‑session branch**: Click **Branch** in top bar → select leaf from `BranchNavigator` dropdown → chat view updates instantly

## Key Source Files

Understanding the implementation requires examining these files from `agegr/pi-web`:

- **[`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts)** – Core logic for both fork and navigate_tree operations
- **[`components/MessageView.tsx`](https://github.com/agegr/pi-web/blob/main/components/MessageView.tsx)** – Fork button UI
- **[`components/BranchNavigator.tsx`](https://github.com/agegr/pi-web/blob/main/components/BranchNavigator.tsx)** – In‑session leaf selector
- **[`components/SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx)** – Forked session display with expand/collapse
- **[`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)** – Context building for consistent fork/navigation targets

Both mechanisms enforce the same safety check: `isBashRunning` must be false to prevent mid-command state corruption.

## Summary

- **Forks** create persistent, independent session files with parent-child relationships visible in the sidebar
- **In‑session branches** provide lightweight, temporary navigation between conversation leaves without file overhead
- Choose **fork** for saved parallel experiments; choose **in‑session branch** for rapid exploration within a single conversation
- Both operations are implemented in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) and blocked during active shell commands

## Frequently Asked Questions

### Can I convert an in‑session branch into a fork?

Not directly through the UI. To persist a branch as its own file, manually fork at the desired leaf point using the **Fork** button, which triggers `createBranchedSession` as shown in [`rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/rpc-manager.ts) lines 399–432.

### Why does forking destroy the current session wrapper?

The `this.destroy()` call in the fork handler ensures clean unloading of the original `AgentSessionWrapper`. This forces the next request to load the newly created session file rather than maintaining stale state.

### Do forked sessions share any storage with their parent?

No. Forked sessions copy history up to the fork point but maintain independent `.jsonl` files. The only link is the `parentSession` metadata header used for sidebar visualization.

### What happens to bash processes during navigation?

Both operations check `this.inner.isBashRunning` and throw if a shell command is active. This prevents state inconsistency when the conversation context changes mid-execution.