# How pi-web Exports a Session to HTML: A Deep Dive into the Server-Side Implementation

> Discover how pi-web exports Pi session files to HTML via its server-side API. Learn about the Pi CLI, dynamic modules, and overflow prevention for deep session trees.

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

---

**Pi-web converts Pi session files (`.jsonl`) to self-contained HTML documents through a server-side API route that uses either the Pi CLI or a fallback dynamic module, with defensive patching to prevent stack overflows on deep session trees.**

The export session to HTML feature in pi-web transforms raw session data into shareable, standalone web pages. This article explores the complete flow—from file resolution to HTTP response—based on the actual source code in the `agegr/pi-web` repository.

## Session File Resolution

The export process begins by locating the session data on disk. The `GET` handler in `app/api/sessions/[id]/export/route.ts` receives a session ID from the URL parameter and calls `resolveSessionPath(id)` from [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) (lines 35-42).

This function maps the session ID to an absolute path pointing to the `.jsonl` file containing the session's serialized state. The resolver handles path validation and ensures the file exists before proceeding.

## HTML Generation: Two Export Paths

Once the session file path is determined, `exportSession()` generates the raw HTML through one of two strategies:

### Primary: Pi CLI Execution

The code first attempts to locate [`cli.js`](https://github.com/agegr/pi-web/blob/main/cli.js) via `getPiCliPath()` (lines 44-78 in [`route.ts`](https://github.com/agegr/pi-web/blob/main/route.ts)). When found, it spawns the CLI using `execFileAsync` with these parameters:

```bash
PI_OFFLINE=1 PI_SKIP_VERSION_CHECK=1 node cli.js --export <session> <output>

```

The environment flags ensure a pure-local export without network calls or version checks. A **30-second hard timeout** prevents hanging processes.

### Fallback: Dynamic Module Import

If the CLI cannot be located, the system dynamically imports `@earendil-works/pi-coding-agent/dist/core/export-html/index.js` and invokes `exportFromFile`. This provides resilience when the CLI isn't available in the deployment environment.

Both paths write output to `os.tmpdir()/pi-web-export` before the next stage begins.

## Patching Recursive Functions for Deep Trees

Raw HTML from Pi contains three recursive helper functions that fail on large sessions:

- `sortChildren` — recursive tree sorting
- `mapNodes` — recursive node mapping  
- `markActive` — recursive active-state marking

The `patchExportHtml()` function (lines 81-115) transforms each into **iterative equivalents**:

```typescript
// Original pattern (recursive, causes stack overflow)
function mapNodes(node, fn) {
  fn(node);
  node.children?.forEach(c => mapNodes(c, fn));
}

// Patched replacement (explicit stack, DFS)
function mapNodes(node, fn) {
  const stack = [node];
  while (stack.length) {
    const n = stack.pop();
    fn(n);
    if (n.children) stack.push(...n.children.reverse());
  }
}

```

For `markActive`, the patch uses a **two-stack post-order walk** to preserve the original traversal semantics. Each replacement validates that exactly one match exists—throwing an error otherwise to detect template drift.

## Secure HTTP Response Delivery

The final stage constructs a properly-typed HTTP response with security headers and flexible disposition:

### Content-Disposition Handling

`getContentDisposition()` (lines 38-42) respects the `inline=1` query flag:

| Query Parameter | Behavior |
|-----------------|----------|
| `inline=1` | Display in browser (`inline`) |
| absent or `0` | Force download (`attachment`) |

The function safely encodes non-ASCII filenames per **RFC 5987**, with ASCII fallback when needed.

### Security Headers

The response includes protective headers (lines 64-75):

```typescript
'Content-Security-Policy': "default-src 'self' 'unsafe-inline' 'unsafe-eval'"
'X-Frame-Options': 'DENY'
'X-Content-Type-Options': 'nosniff'

```

These prevent clickjacking via framing and block MIME-type sniffing attacks.

## Complete API Request Examples

### Inline View (Browser)

```javascript
fetch('/api/sessions/3f5b2c4a-9e1d-4a6e-8c2f/export?inline=1')
  .then(r => r.text())
  .then(html => {
    const w = window.open();
    w.document.write(html);
  });

```

### Download via cURL

```bash
curl -L -O \
  "http://127.0.0.1:30141/api/sessions/3f5b2c4a-9e1d-4a6e-8c2f/export"

```

### Programmatic Node.js (Attachment)

```javascript
const https = require('https');
https.get('http://127.0.0.1:30141/api/sessions/abc123/export', res => {
  const chunks = [];
  res.on('data', d => chunks.push(d));
  res.on('end', () => {
    const html = Buffer.concat(chunks);
    require('fs').writeFileSync('session.html', html);
  });
});

```

## Key Implementation Files

| Component | Location |
|-----------|----------|
| Export route handler | `app/api/sessions/[id]/export/route.ts` |
| Session path resolver | [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) (lines 35-42) |
| CLI discovery logic | [`route.ts`](https://github.com/agegr/pi-web/blob/main/route.ts) lines 44-78 |
| HTML patching (iterative recursion) | [`route.ts`](https://github.com/agegr/pi-web/blob/main/route.ts) lines 81-115 |
| Content-Disposition builder | [`route.ts`](https://github.com/agegr/pi-web/blob/main/route.ts) lines 38-42 |
| External export core (dynamic import) | `@earendil-works/pi-coding-agent/dist/core/export-html/index.js` |

## Summary

- **File resolution** uses `resolveSessionPath()` to locate `.jsonl` session files
- **Dual export strategy** tries Pi CLI first, falls back to dynamic module import
- **Stack-overflow protection** replaces recursive tree functions with iterative DFS implementations
- **Secure delivery** includes RFC 5987 filename encoding and multiple security headers
- **Flexible disposition** supports both inline viewing and forced download via query parameter

## Frequently Asked Questions

### What causes stack overflow in the original Pi HTML export?

The raw HTML contains recursive implementations of `sortChildren`, `mapNodes`, and `markActive`. Sessions with over 5,000 linear entries exceed JavaScript's call stack limit. The `patchExportHtml()` function replaces these with explicit-stack iterative versions.

### Why does pi-web prefer the Pi CLI over the dynamic module?

The CLI path provides better isolation and aligns with Pi's official export tooling. The dynamic import exists as a **fallback** when the CLI isn't present in the deployment environment, ensuring the feature works across different installation scenarios.

### How does the `inline` query parameter affect behavior?

When `inline=1` is present, the `Content-Disposition` header uses `inline` mode, allowing browsers to display the HTML directly. Without this flag, the header uses `attachment` mode, triggering a file download. The filename is safely encoded for non-ASCII characters per RFC 5987.