# How to Export Session HTML in Pi Web: Recursive Tree Handling Explained

> Export Pi Web session HTML with the GET /api/sessions/[id]/export endpoint. Learn how recursive tree handling prevents stack overflows for large sessions.

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

---

**Pi Web provides a GET `/api/sessions/[id]/export` endpoint that converts Pi session files into self-contained HTML exports, applying a runtime patch that transforms recursive tree traversal functions into iterative implementations to prevent stack overflows on sessions with 5,000+ entries.**

The `agegr/pi-web` repository implements a robust **exporting session HTML** pipeline designed to handle arbitrarily deep session histories without browser crashes. When users request an HTML export of their Pi coding session, the system orchestrates file resolution, CLI execution, and critical runtime patching to ensure reliable rendering.

## The Session HTML Export Process

### 1. Locating the Session File

The process begins in `app/api/sessions/[id]/export/route.ts` by calling `resolveSessionPath()` from **[`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)**. This function maps the requested session ID to its absolute file path on disk.

```ts
// lib/session-reader.ts
export async function resolveSessionPath(sessionId: string): Promise<string | null> { … }

```

### 2. Running the Pi Coding Agent Exporter

The endpoint attempts to locate the **pi-coding-agent CLI** ([`cli.js`](https://github.com/agegr/pi-web/blob/main/cli.js)) using `getPiCliPath()`, which searches several possible installation locations. If found, the system spawns the CLI via `execFileAsync` with the `--export <input> <output>` arguments.

If the CLI cannot be located, the implementation falls back to the pure-JS exporter bundled with the package, dynamically importing `exportFromFile` and invoking it directly.

```ts
// app/api/sessions/[id]/export/route.ts
const cliPath = await getPiCliPath();
if (cliPath) {
  // execFileAsync invocation
}
else {
  const { exportFromFile } = await import(exporterUrl);
  // Direct function call
}

```

### 3. Patching Recursive Tree Functions

The original [`template.js`](https://github.com/agegr/pi-web/blob/main/template.js) bundled with *pi-coding-agent* uses **recursive helpers**—`sortChildren`, `mapNodes`, and `markActive`—to walk the session tree. On very deep linear sessions (≥ 5,000 entries), these recursions overflow the JavaScript call stack.

The `patchExportHtml()` function, defined in `app/api/sessions/[id]/export/route.ts`, performs three critical textual replacements to convert each recursive function into an **iterative version** using an explicit stack. It first normalizes line endings (`\r\n → \n`) to ensure cross-platform compatibility.

```ts
// app/api/sessions/[id]/export/route.ts
function patchExportHtml(html: string): string {
  const n = (s: string) => s.replace(/\r\n/g, "\n");
  html = n(html);
  // …replaceRequired calls for sortChildren, mapNodes, markActive…
}

```

The specific transformations include:

- **`sortChildren`** → Depth-first stack implementation that sorts children before pushing them onto the stack.
- **`mapNodes`** → Stack-based iteration that builds `treeNodeMap` without recursion.
- **`markActive`** → Two-stack post-order traversal that propagates the "active-path" flag through the tree.

This patch guarantees that the exported HTML functions correctly for arbitrarily deep session trees.

### 4. Returning the HTML Response

After patching, the endpoint streams the HTML back with precise HTTP headers constructed in the GET handler:

- `Content-Type: text/html; charset=utf-8`
- `Content-Disposition` (inline or attachment) generated by `getContentDisposition()`, which safely encodes non-ASCII filenames
- `Cache-Control: no-cache`

The disposition type depends on the query parameter: if `?inline=1` is present, the content displays inline; otherwise, it downloads as an attachment.

```ts
// app/api/sessions/[id]/export/route.ts (GET handler)
return new Response(patchedHtml, {
  headers: {
    "Content-Type": "text/html; charset=utf-8",
    "Content-Disposition": getContentDisposition(fileName, inline),
    "Cache-Control": "no-cache",
  },
});

```

## Session HTML Export Implementation Examples

### Client-Side Inline Viewing

To display the exported HTML directly in a new browser window:

```ts
// Request the export as an inline HTML page
fetch(`/api/sessions/${sessionId}/export?inline=1`)
  .then(r => r.text())
  .then(html => {
    const win = window.open();
    win!.document.write(html);
  });

```

### Command-Line Download

To download the export as a file using curl:

```bash
curl -L "https://your-pi-web-instance/api/sessions/${sessionId}/export" \
  -o pi-session.html

```

### Manual Patch Application

For custom tooling that processes existing exported HTML files:

```ts
import { readFileSync } from "fs";
import { patchExportHtml } from "./app/api/sessions/[id]/export/route";

const rawHtml = readFileSync("session.html", "utf8");
const safeHtml = patchExportHtml(rawHtml);
// safeHtml now contains iterative tree helpers and can be saved or displayed.

```

## Summary

- **Pi Web** converts session files to HTML via the `GET /api/sessions/[id]/export` endpoint implemented in `app/api/sessions/[id]/export/route.ts`.
- The system first resolves the session file path using `resolveSessionPath()` from [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts).
- It attempts to use the **pi-coding-agent CLI** for export, falling back to a pure-JS implementation if the CLI is unavailable.
- A critical **patching mechanism** transforms recursive tree functions (`sortChildren`, `mapNodes`, `markActive`) into iterative versions to prevent stack overflows on deep sessions.
- The endpoint supports both inline viewing (`?inline=1`) and attachment downloads with proper `Content-Disposition` encoding.

## Frequently Asked Questions

### Why does Pi Web patch the exported HTML instead of modifying the source exporter?

The HTML patch in `app/api/sessions/[id]/export/route.ts` addresses a specific runtime limitation: the **pi-coding-agent**'s original [`template.js`](https://github.com/agegr/pi-web/blob/main/template.js) uses recursive tree traversals that crash on sessions exceeding 5,000 entries due to JavaScript call stack limits. By applying textual replacements to convert these to iterative algorithms after generation, Pi Web ensures compatibility with arbitrarily deep session trees without requiring modifications to the upstream exporter package.

### Which recursive functions are converted to iterative implementations?

The `patchExportHtml()` function specifically targets three recursive helpers in the generated HTML: **`sortChildren`** (converted to depth-first stack sorting), **`mapNodes`** (converted to stack-based tree node mapping), and **`markActive`** (converted to two-stack post-order traversal for active path propagation). Each transformation replaces function recursion with explicit stack management.

### How does the export endpoint handle missing Pi CLI dependencies?

According to the source code in `app/api/sessions/[id]/export/route.ts`, the endpoint first attempts to locate [`cli.js`](https://github.com/agegr/pi-web/blob/main/cli.js) via `getPiCliPath()`. If the **pi-coding-agent CLI** is not found in expected installation locations, the system dynamically imports the `exportFromFile` function from the package's pure-JS exporter using `await import(exporterUrl)`, ensuring the export functionality works even without the CLI binary.

### Can I force the browser to display the HTML instead of downloading it?

Yes. Append `?inline=1` to the export URL query string. The route handler checks this parameter to determine the `Content-Disposition` header value: when `inline` is true, the browser renders the HTML directly; otherwise, the `getContentDisposition()` function triggers a file download with the session filename properly encoded for non-ASCII characters.