# How Session Export Generates HTML and Uses Iterative Tree Helpers in Pi-Web

> Discover how session export generates HTML in Pi-Web using the pi-coding-agent CLI and iterative tree helpers to avoid stack overflows with deep trees.

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

---

**Session export generates HTML by locating the session file, running the pi-coding-agent CLI or export-html module to build raw HTML, then patching recursive tree helpers into iterative versions to prevent stack overflow on deep trees.**

The `pi-web` repository implements a robust session export endpoint that transforms stored session data into visual HTML reports. According to the agegr/pi-web source code, this process involves a deliberate three-stage pipeline designed to handle arbitrarily deep conversation trees without crashing the browser.

## The Three-Stage Export Pipeline

The `GET /api/sessions/[id]/export` endpoint in `app/api/sessions/[id]/export/route.ts` orchestrates the complete flow through distinct phases.

### Stage 1: Session File Resolution

The handler begins by locating the persisted session on disk. The `resolveSessionPath(id)` utility from [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) returns the absolute path to the `.jsonl` file containing the session's message history.

```typescript
// app/api/sessions/[id]/export/route.ts#L48-L53
const sessionPath = await resolveSessionPath(id);
if (!sessionPath) {
  return NextResponse.json({ error: 'Session not found' }, { status: 404 });
}

```

### Stage 2: HTML Generation via CLI or Module

The system attempts two generation strategies in sequence:

**`pi-coding-agent` CLI (preferred):** If the CLI binary is available, `exportSession` spawns it via `execFileAsync` to produce the HTML file.

```typescript
// app/api/sessions/[id]/export/route.ts#L17-L30
await execFileAsync('pi-coding-agent', ['--export', sessionPath, tempHtmlPath]);

```

**Fallback to export-html module:** When the CLI is missing, the code dynamically imports `node_modules/@earendil-works/pi-coding-agent/dist/core/export-html/index.js` and invokes `exportFromFile` directly.

```typescript
// app/api/sessions/[id]/export/route.ts#L33-L38
const { exportFromFile } = await import(
  '@earendil-works/pi-coding-agent/dist/core/export-html/index.js'
);
await exportFromFile(sessionPath, tempHtmlPath);

```

Both paths produce identical HTML containing the session messages and a visual tree rendered by the pi-coding-agent template.

### Stage 3: Patching Recursive Helpers to Iterative Forms

Before returning the HTML, `patchExportHtml` transforms three recursive tree traversal functions into iterative equivalents. This prevents **call stack overflow** on deeply linear session trees exceeding 5,000 entries.

## The Three Iterative Tree Helpers

The patch targets specific recursive implementations embedded in the generated JavaScript. Each replacement maintains identical semantics while eliminating recursion.

### `sortChildren`: DFS Pre-Order with Explicit Stack

| Aspect | Recursive Original | Iterative Replacement |
|--------|-------------------|----------------------|
| **Purpose** | Sort each node's children by timestamp, then recurse | Same sorting logic, stack-based traversal |
| **Mechanism** | Direct function calls on children | Push children to stack, process in LIFO order |

**Iterative implementation:**

```javascript
// Iterative DFS pre-order for sortChildren
const stack = [node];
while (stack.length > 0) {
  const current = stack.pop();
  current.children.sort((a, b) => a.timestamp - b.timestamp);
  // Push children in reverse order to maintain left-to-right processing
  for (let i = current.children.length - 1; i >= 0; i--) {
    stack.push(current.children[i]);
  }
}

```

As implemented in the patch source (`app/api/sessions/[id]/export/route.ts#L49-L60`), this uses an explicit stack array where nodes are popped, sorted, and their children pushed for subsequent processing.

### `mapNodes`: Reversed Root Stack for ID Mapping

The recursive `mapNodes` builds a `Map` from entry IDs to tree nodes through depth-first traversal.

**Iterative replacement (`L71-L78`):**

```javascript
// Start with reversed tree array to maintain processing order
const stack = [...tree].reverse();
while (stack.length > 0) {
  const node = stack.pop();
  map.set(node.id, node);
  // Push children in reverse to process them in original order
  for (let i = node.children.length - 1; i >= 0; i--) {
    stack.push(node.children[i]);
  }
}

```

The stack initialization with `[...tree].reverse()` ensures the iterative version produces identical output to the recursive implementation.

### `markActive`: Two-Stack Post-Order Traversal

The most complex replacement achieves **post-order traversal** (children processed before parent) without recursion. This is essential for correctly flagging the active path in the UI.

**Implementation (`L92-L110`):**

```javascript
// First stack for traversal, second to reverse order for post-order processing
const stack1 = [node];
const stack2 = [];
while (stack1.length > 0) {
  const current = stack1.pop();
  stack2.push(current);
  // Push children to first stack in normal order
  for (const child of current.children) {
    stack1.push(child);
  }
}
// Process stack2 (now in reverse post-order)
while (stack2.length > 0) {
  const current = stack2.pop();
  // Mark active logic here: children already processed
}

```

This classic two-stack technique builds `stack2` in root-right-left order, then popping yields left-right-root—the standard post-order sequence.

## How the Patch Applies Transformations

The `patchExportHtml` function performs precise string replacement on the generated HTML:

```typescript
// app/api/sessions/[id]/export/route.ts#L30-L38
let html = await fs.readFile(tempHtmlPath, 'utf-8');
// Normalize line endings to guarantee single match
html = html.replace(/\r\n/g, '\n');

// Apply three iterative replacements
html = html.replace(recursiveSortChildrenPattern, iterativeSortChildren);
html = html.replace(recursiveMapNodesPattern, iterativeMapNodes);
html = html.replace(recursiveMarkActivePattern, iterativeMarkActive);

```

Safety checks throw if any pattern fails to match exactly once (`L34-L36`), ensuring the HTML structure matches expectations.

## Returning the Final Response

After patching, the handler sets security headers and streams the result:

```typescript
// app/api/sessions/[id]/export/route.ts#L66-L74
const headers = new Headers({
  'Content-Type': 'text/html; charset=utf-8',
  'Content-Security-Policy': "default-src 'self'; script-src 'unsafe-inline'...",
});
if (!inline) {
  headers.set('Content-Disposition', `attachment; filename="session-${id}.html"`);
}
return new NextResponse(html, { headers });

```

## Complete Export Handler Example

```typescript
// Using the export endpoint programmatically
import { GET as exportSessionHandler } from '@/app/api/sessions/[id]/export/route';

const request = new Request(
  'http://localhost/api/sessions/abcd1234/export?inline=1'
);
const response = await exportSessionHandler(
  request,
  { params: Promise.resolve({ id: 'abcd1234' }) }
);

const safeHtml = await response.text();
// safeHtml contains iterative tree helpers, safe for arbitrary tree depth

```

## Summary

- **`resolveSessionPath`** locates the `.jsonl` session file in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts).
- **HTML generation** prefers the `pi-coding-agent` CLI, falling back to the `exportFromFile` module function.
- **`patchExportHtml`** transforms three recursive helpers (`sortChildren`, `mapNodes`, `markActive`) into iterative versions using explicit stacks and two-stack post-order traversal.
- **Line ending normalization** ensures reliable pattern matching before replacement.
- **Strict validation** throws on unexpected match counts to prevent silent failures.

## Frequently Asked Questions

### What causes stack overflow in the original recursive helpers?

The original helpers use JavaScript function recursion to traverse tree structures. On session trees with deeply linear chains (thousands of nested entries), the call stack exceeds V8's ~10,000 frame limit, causing `RangeError: Maximum call stack size exceeded`. The iterative replacements bound memory usage to heap-allocated arrays instead.

### Why use a two-stack approach for post-order traversal?

Post-order processing requires children to complete before their parent. A single stack cannot directly achieve this without recursion. The two-stack method first builds a "reverse post-order" sequence (root, right, left), then reversing via a second stack yields the required left, right, root order—mathematically equivalent but iteratively implementable.

### Can the CLI and module generate different HTML output?

No. Both the `pi-coding-agent` CLI and the `exportFromFile` module reference the same template code in `node_modules/@earendil-works/pi-coding-agent/dist/core/export-html/index.js`. The CLI is a thin wrapper for environments where Node.js module resolution is inconvenient; the fallback import executes identical rendering logic directly.

### What happens if a helper pattern doesn't match?

The patch throws an explicit error (`L34-L36`) indicating which helper failed to patch. This defensive design prevents serving HTML with vulnerable recursive implementations when the template structure changes unexpectedly. The error message includes the expected pattern for debugging template version mismatches.