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

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. This function maps the requested session ID to its absolute file path on disk.

// 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) 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.

// 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 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.

// 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.

// 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:

// 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:

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:

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.
  • 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →