How pi-web Implements File Explorer with Directory Browsing and Path Encoding

The pi-web file explorer uses a lazy-loading React tree component combined with centralized path encoding utilities to deliver secure, cross-platform directory browsing.

The pi-web file explorer provides a responsive sidebar interface for navigating project directories without loading the entire file tree upfront. Built with React and TypeScript, it delegates all path manipulation to a dedicated utility module while fetching directory contents on-demand via a REST API. This architecture ensures consistent behavior across Windows, macOS, and Linux while keeping network traffic minimal.

Core Architecture

The implementation separates concerns between the user interface and path handling logic. The FileExplorer component in components/FileExplorer.tsx renders the interactive tree view, while lib/file-paths.ts contains all platform-specific path normalization and encoding logic.

The UI consists of two main parts:

  • FileExplorer – The outer React component that manages state for the current working directory (cwd), expanded paths, and Git status integration.
  • TreeNode – An inner component that renders individual file and folder entries, handling expansion toggles and lazy loading of child entries.

When a user interacts with the tree, the component calls fetchEntries (lines 72–94 of FileExplorer.tsx), which communicates with the backend endpoint /api/files/<encodedPath>?type=list. The backend route handler at app/api/files/[...path].ts serves the directory listing, while the client handles rendering and caching.

Lazy Directory Browsing

The explorer implements lazy loading to avoid fetching the entire repository structure on initial load. The loading sequence follows this lifecycle:

  1. Initial Mount – A useEffect hook monitored by [cwd, refreshKey, treeRefreshKey] triggers the root directory load (lines 106–109).
  2. Root Render – Root-level entries render as TreeNode components with collapsed state by default.
  3. Expansion Trigger – Clicking a folder toggles its state in expandedPaths. If the folder opens for the first time (!loaded), the loadChildren function executes (lines 21–33).
  4. Data Fetching – Inside loadChildren, the component calls fetchEntries(node.fullPath) to retrieve directory contents via the API.
  5. Recursive Rendering – Child entries render only when open && node.isDir evaluates true, repeating the lazy-load pattern for nested directories.

This approach keeps initial page loads fast and memory usage low, even for repositories containing thousands of files.

Path Encoding and Normalization

All API interactions rely on encodeFilePathForApi from lib/file-paths.ts (lines 8–14) to prevent URL encoding errors and handle cross-platform path differences:

export function encodeFilePathForApi(filePath: string): string {
  return normalizeFilePathSlashes(filePath)
    .split("/")
    .filter(Boolean)
    .map(encodeURIComponent)
    .join("/");
}

The function executes four critical steps:

  • Normalization – Converts Windows backslashes (\) to forward slashes using normalizeFilePathSlashes (lines 1–6).
  • Segmentation – Splits on / and filters empty strings to remove leading or double slashes.
  • URL Encoding – Applies encodeURIComponent to each segment, safely escaping spaces, hashes, and question marks.
  • Reconstruction – Joins segments back with / to produce a clean path string suitable for HTTP requests.

The resulting encoded path interpolates directly into API calls:

const encoded = encodeFilePathForApi(dirPath);
const res = await fetch(`/api/files/${encoded}?type=list`);

Path Utilities Integration

Beyond encoding, lib/file-paths.ts provides several utilities used throughout the component. joinFilePath (lines 21–44) combines parent directories with child names while maintaining platform-agnostic formatting:

const childPath = joinFilePath(dirPath, entry.name); // Used at line 88

For chat integrations, getRelativeFilePath derives paths relative to the current working directory (used at line 145). This powers the "mention" feature that inserts file references into conversations.

The module also ensures Git status consistency. Before storing Git file paths in a Map, the code applies normalizeFilePathSlashes to align repository paths with the explorer's canonical format, preventing mismatch errors between the file tree and version control indicators.

Implementation Example

To integrate the file explorer into a page, import the FileExplorer component and provide callback handlers for file interactions:

import { FileExplorer } from "@/components/FileExplorer";

export default function Sidebar({ cwd }: { cwd: string }) {
  const handleOpenFile = (fullPath: string, name: string) => {
    console.log("Open:", fullPath);
  };

  const handleAtMention = (relative: string, isDir: boolean) => {
    console.log("Mention:", relative, isDir ? "(dir)" : "(file)");
  };

  return (
    <FileExplorer
      cwd={cwd}
      onOpenFile={handleOpenFile}
      onAtMention={handleAtMention}
    />
  );
}

The component automatically handles file uploads through a hidden <input type="file"> element and displays Git status indicators alongside file entries.

Summary

  • Lazy Loading Architecture – The FileExplorer component fetches directory contents only when nodes expand, minimizing network traffic for large repositories.
  • Centralized Path Logic – All path operations reside in lib/file-paths.ts, ensuring consistent handling of Windows, macOS, and Linux file systems.
  • Secure API Communication – The encodeFilePathForApi function normalizes slashes and URL-encodes each path segment to prevent injection and parsing errors.
  • Git Integration – Normalized path handling aligns the file explorer with Git status data, providing accurate version control indicators in the UI.

Frequently Asked Questions

How does pi-web handle Windows file paths in the file explorer?

The normalizeFilePathSlashes utility in lib/file-paths.ts converts all Windows backslashes to forward slashes before any processing occurs. This normalization happens at the boundary of API calls and UI rendering, ensuring the internal logic and API endpoints consistently use Unix-style paths regardless of the host operating system.

What triggers a directory to load its contents?

Directories load when a user clicks the expand arrow for the first time. The TreeNode component checks its loaded state, and if false, executes fetchEntries with the node's full path. Subsequent expand/collapse actions toggle visibility without re-fetching, unless a manual refresh key triggers an update.

How does the path encoding prevent URL errors?

The encodeFilePathForApi function splits paths into segments and applies encodeURIComponent to each piece individually. This prevents reserved characters like #, ?, and spaces from breaking the URL structure, while preserving forward slashes as valid path separators between encoded segments.

Where is the directory listing API implemented?

While the client-side code in components/FileExplorer.tsx consumes the API, the server-side handler resides at app/api/files/[...path].ts. This Next.js catch-all route handles GET requests with type=list to return directory contents, utilizing the same path encoding logic to decode and resolve file system paths securely.

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 →