# How the Pi-Web Chat Minimap Enables Fast Navigation of Long Sessions

> Discover how the pi-web chat minimap uses React and scroll calculations to enable lightning-fast navigation through long conversation sessions, improving user experience.

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

---

**The pi-web chat minimap is a lightweight React component that renders a clickable overview bar next to message lists, allowing users to jump to any point in lengthy conversations through real-time scroll ratio calculations and throttled DOM measurements.**

The chat minimap in the [agegr/pi-web](https://github.com/agegr/pi-web) repository solves the usability challenge of navigating extensive AI conversations without excessive scrolling. Implemented primarily in [`components/ChatMinimap.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatMinimap.tsx), this navigation aid provides a spatial overview of the entire session while maintaining 60fps performance through optimized measurement strategies and `ResizeObserver` integration.

## Architecture Overview

The minimap implementation revolves around three core inputs: the static `messages` array, an optional `streamingMessage` representing the assistant’s in-flight response, and a reference to the scrollable container. The component `ChatMinimap` receives these via props alongside `messageRefs`, an array of refs pointing to each rendered message element created by the exported `useMessageRefs` hook (lines 400-405).

Rather than reading DOM dimensions during every scroll event, the architecture separates **scroll tracking** (cheap, continuous) from **node measurement** (expensive, throttled). This ensures the UI remains responsive even when rendering hundreds of messages.

## Core Implementation Mechanics

### Message Collection and State Management

The component first normalizes its data sources. Inside [`ChatMinimap.tsx`](https://github.com/agegr/pi-web/blob/main/ChatMinimap.tsx), a `useMemo` hook merges the static `messages` array with the optional `streamingMessage` to create `allMessages` (lines 77-80):

```typescript
const allMessages = useMemo(
  () => (streamingMessage ? [...messages, streamingMessage] : messages),
  [messages, streamingMessage]
);

```

To maintain stable references across re-renders, the companion hook `useMessageRefs` generates a memoized array of refs whose length matches the message count. [`ChatWindow.tsx`](https://github.com/agegr/pi-web/blob/main/ChatWindow.tsx) consumes this hook and passes the resulting refs into both the message list and the minimap, ensuring both components point to the same DOM elements.

### Scroll Tracking Without DOM Thrashing

Performance is preserved by avoiding DOM reads during scroll events. The `updateScroll` callback (lines 84-99) calculates two critical ratios based on the container’s `scrollTop`, `scrollHeight`, and `clientHeight`:

- **`scrollRatio`**: How far the user has scrolled from the top (0 to 1)
- **`viewportRatio`**: The proportion of the total content currently visible

These ratios drive the visual viewport indicator without touching the message DOM.

### Throttled Node Measurement

When the layout changes—whether from new messages, content expansion, or container resizing—the `measureNodes` function (lines 101-138) executes to map the visual layout. This function is throttled to approximately 150ms to prevent layout thrashing.

`measureNodes` walks the `messageRefs` array and reads each element’s bounding box relative to the scroll container. It calculates and stores:

- **Top ratio**: The message’s vertical position as a percentage of total height
- **Height ratio**: The message’s height as a percentage of total content height

Only messages containing actual text (user or assistant roles) are recorded, with results stored in the component’s `nodes` state.

### Layout Synchronization

To keep the minimap accurate when messages expand or collapse, a `ResizeObserver` watches both the scroll container and its first child (lines 149-169). When mutations occur, the observer triggers both `updateScroll` and `measureNodes`, ensuring the overview remains synchronized with the actual content geometry.

## Rendering the Visual Overview

The minimap renders as a fixed-width bar (`MINIMAP_WIDTH = 36` pixels) alongside the chat history. For each entry in the `nodes` state, the component draws a dot whose color encodes the message role: **blue** for `user` and **gray** for `assistant` (within the JSX block, lines 96-135).

Dot sizing is dynamic—the message nearest to the mouse cursor receives an enlarged visual treatment to aid targeting. A translucent box representing the visible viewport overlays the minimap, positioned using `scrollRatio` and `viewportRatio` (lines 80-90).

## Interactive Navigation

### Click and Drag Scrolling

User interaction is handled by `handleMouseDown` (lines 89-113), which calculates a click ratio based on the mouse position within the minimap. The function offsets this ratio by the current viewport size and invokes `scrollToMinimapRatio` to programmatically scroll the container to the matching content position.

Global `mousemove` and `mouseup` listeners maintain scroll synchronization while the user drags, creating a fluid scrubbing experience across the conversation timeline.

### Smart Tooltip Positioning

When hovering over the minimap, message previews appear to the left. The `tooltipPositions` memo (lines 222-244) first computes raw tooltip coordinates based on node positions, then executes a collision-detection pass. A "push-apart" algorithm runs up to ten iterations to resolve overlaps between adjacent tooltips, finally clamping positions to the minimap’s height boundaries to keep previews fully visible.

## Integration Example

To embed the minimap in a chat view, import both the component and the ref hook from [`components/ChatMinimap.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatMinimap.tsx):

```tsx
import { ChatMinimap, useMessageRefs } from "./ChatMinimap";

export default function ChatWindow({ sessionId }: { sessionId: string }) {
  const { messages, streamingMessage } = useAgentSession(sessionId);
  const scrollContainer = useRef<HTMLDivElement>(null);
  const messageRefs = useMessageRefs(messages.length);

  return (
    <div className="flex">
      {/* Message list */}
      <div ref={scrollContainer} className="flex-1 overflow-y-auto">
        {messages.map((msg, i) => (
          <MessageView
            key={msg.id}
            message={msg}
            ref={el => (messageRefs.current[i] = el)}
          />
        ))}
        {streamingMessage && <MessageView message={streamingMessage} />}
      </div>

      {/* Minimap */}
      <ChatMinimap
        messages={messages}
        streamingMessage={streamingMessage}
        scrollContainer={scrollContainer}
        messageRefs={messageRefs}
      />
    </div>
  );
}

```

For any component rendering a message list, create stable refs using:

```tsx
const refs = useMessageRefs(messageCount);
// Assign to elements:
<div ref={el => (refs.current[i] = el)} />

```

## Summary

- The **pi-web chat minimap** provides spatial navigation for long sessions through a fixed-width overview bar rendered alongside the message list.
- **Performance is optimized** by separating continuous scroll ratio calculations from throttled DOM measurements (150ms) and utilizing `ResizeObserver` for layout changes.
- **Navigation relies on ratio-based positioning**, mapping click coordinates on the minimap to scroll positions in the content via `scrollToMinimapRatio`.
- **Visual feedback includes** role-colored dots (blue for user, gray for assistant), a dynamic viewport indicator, and collision-aware tooltips using a push-apart algorithm.
- **Integration requires** the `useMessageRefs` hook to maintain stable element references between the message list and minimap components.

## Frequently Asked Questions

### How does the chat minimap maintain performance during fast scrolling?

The component avoids DOM measurement during scroll events by calculating abstract `scrollRatio` and `viewportRatio` values from the container’s scroll properties. Expensive DOM reads via `measureNodes` are throttled to approximately 150ms, ensuring the scroll thread never blocks while the visual indicator updates continuously.

### What triggers the minimap to recalculate message positions?

Recalculation occurs through two mechanisms: a `ResizeObserver` that monitors the scroll container and its children for height changes (lines 149-169), and internal state updates when new messages arrive. Whenever the message list grows or content expands, the observer triggers `measureNodes` to refresh the top and height ratios stored in the `nodes` state.

### How does the minimap handle streaming messages in real-time?

The `allMessages` memo (lines 77-80) concatenates the static `messages` array with the `streamingMessage` when present. This unified array drives both the message list rendering and the minimap’s node measurements, allowing the overview bar to grow dynamically as the assistant generates content without requiring separate handling for streaming states.

### Can the chat minimap be used independently of the ChatWindow component?

Yes, provided the consuming component supplies the three required dependencies: the `messages` array (plus optional `streamingMessage`), a `RefObject` pointing to the scrollable container, and a `messageRefs` array generated by `useMessageRefs`. The minimap is decoupled from specific data sources like [`useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/useAgentSession.ts), making it reusable across different chat implementations within the codebase.