How the Pi-Web Chat Minimap Enables Fast Navigation of Long Sessions
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 repository solves the usability challenge of navigating extensive AI conversations without excessive scrolling. Implemented primarily in 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, a useMemo hook merges the static messages array with the optional streamingMessage to create allMessages (lines 77-80):
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 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:
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:
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
ResizeObserverfor 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
useMessageRefshook 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, making it reusable across different chat implementations within the codebase.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →