How the Session Context API Loads Branch-Specific Message Trees in pi-web
The session context API in pi-web loads branch-specific message trees by accepting a leafId query parameter that identifies the target conversation branch, delegating to the Pi SDK's piBuildSessionContext function to filter entries, and converting the resulting entries into UI-ready messages via buildSessionContext in lib/session-reader.ts.
The pi-web repository implements a sophisticated branching conversation interface for AI coding sessions. When users navigate between different branches of a session tree, the session context API efficiently loads only the messages specific to that branch by leveraging the Pi SDK's tree-walking capabilities and transforming raw session entries into UI-ready formats.
Initiating the Context Request from the Client
The request originates in hooks/useAgentSession.ts, where the loadContext callback constructs the API endpoint whenever the active leaf changes:
// hooks/useAgentSession.ts – lines 502-508
const loadContext = useCallback(async (sid: string, leafId: string | null) => {
const params = new URLSearchParams({ deferThinking: "1", deferMedia: "1" });
if (leafId) params.set("leafId", leafId);
const url = `/api/sessions/${encodeURIComponent(sid)}/context?${params}`;
const res = await fetch(url);
const d = await res.json() as { context: { messages: AgentMessage[]; entryIds: string[] } };
setMessages(d.context.messages);
setEntryIds(d.context.entryIds ?? []);
}, []);
The leafId query parameter identifies the tip of the branch the UI wants to display. When provided, the API returns only the ancestor entries leading to that leaf, rather than the entire session history.
API Route Handling and Session Resolution
The server-side handler resides in app/api/sessions/[id]/context/route.ts. This route extracts the leafId from the query string, resolves the session's on-disk file path, and initializes a session manager instance:
// app/api/sessions/[id]/context/route.ts – lines 10-27
const leafId = url.searchParams.get("leafId") ?? undefined;
const deferThinking = url.searchParams.has("deferThinking");
const deferToolResultImages = url.searchParams.has("deferMedia");
const filePath = await resolveSessionPath(id);
const sm = SessionManager.open(filePath);
const context = buildSessionContext(
sm.getEntries() as never,
leafId,
{ deferThinking, deferToolResultImages }
);
return NextResponse.json({ context });
The resolveSessionPath function maps the public session ID to the underlying JSONL file. SessionManager.open provides access to the raw session entries via the Pi SDK, which are then passed to buildSessionContext along with the target leaf identifier.
Building the Branch-Specific Message Tree
The lib/session-reader.ts file contains the core logic for transforming raw SDK entries into a branch-specific UI context. The buildSessionContext function orchestrates three distinct phases: entry indexing, branch selection, and message conversion.
Indexing Entries for Branch Lookup
The function begins by indexing all session entries by their ID to enable efficient tree traversal:
// lib/session-reader.ts – lines 4-8
const byId = new Map<string, SessionEntry>();
for (const e of entries) byId.set(e.id, e);
This byId map is passed to the Pi SDK functions to resolve parent-child relationships during branch selection.
Delegating Branch Selection to the Pi SDK
After indexing, the function invokes the Pi SDK to determine which entries belong to the requested branch:
// lib/session-reader.ts – lines 206-219
const piCtx = piBuildSessionContext(
piEntries,
leafId,
byId as unknown as Map<string, PiSessionEntry>
);
// Build the ordered list of context entries
const contextEntries = piBuildContextEntries(
piEntries,
leafId,
byId as unknown as Map<string, PiSessionEntry>,
);
The piBuildSessionContext and piBuildContextEntries functions (from the @earendil-works/pi-coding-agent SDK) walk the session tree using the provided leafId as the target node. They return only the entries that form the path from the session root to the specified leaf, handling compaction markers and branching rules internally.
Converting Entries to UI-Ready Messages
After obtaining the filtered entry list, the function converts each SDK entry into the AgentMessage format expected by the React UI. The entryToUiMessage helper filters out non-message entries and processes content based on the deferral options:
// lib/session-reader.ts – lines 93-130 (excerpt)
function entryToUiMessage(
entry: SessionEntry,
options: { deferThinking?: boolean; deferToolResultImages?: boolean },
): AgentMessage | null {
switch (entry.type) {
case "message": {
const message = options.deferToolResultImages
? omitToolResultBase64Images(normalizeToolCalls(entry.message))
: normalizeToolCalls(entry.message);
if (!options.deferThinking || message.role !== "assistant") return message;
// Strip "thinking" blocks if defer-thinking is requested...
}
default:
return null;
}
}
The conversion logic uses normalizeToolCalls to standardize tool payloads and omitToolResultBase64Images to strip heavy image data when deferMedia is enabled. The resulting messages array together with entryIds is returned to the API route.
Synchronizing UI State After Response
Upon receiving the JSON response, the client updates the React state to display the branch-specific conversation:
// hooks/useAgentSession.ts – lines 511-515
const d = await res.json() as { context: { messages: AgentMessage[]; entryIds: string[] } };
setMessages(d.context.messages);
setEntryIds(d.context.entryIds ?? []);
This state update triggers a re-render of the chat interface, displaying only the messages belonging to the selected branch while maintaining the full session tree in the sidebar navigation.
Summary
- The session context API accepts a
leafIdparameter atGET /api/sessions/[id]/contextto identify specific conversation branches. buildSessionContextinlib/session-reader.tsdelegates branch filtering to the Pi SDK'spiBuildSessionContextandpiBuildContextEntriesfunctions.- Raw session entries are converted to UI-ready
AgentMessageobjects byentryToUiMessage, which handles tool call normalization and optional media deferral vianormalizeToolCallsandomitToolResultBase64Images. - The client-side
loadContextfunction inhooks/useAgentSession.tsmanages the request lifecycle and state synchronization using thedeferThinkinganddeferMediaflags. - This architecture allows pi-web to display isolated branch views without reloading the entire session file or processing unrelated conversation history.
Frequently Asked Questions
What happens if no leafId is provided to the session context API?
If the leafId query parameter is omitted, the Pi SDK defaults to returning the currently active branch of the session (typically the most recent leaf). The buildSessionContext function passes undefined to the SDK, which then selects the default branch path based on session state rather than a specific target node.
How does the Pi SDK determine which entries belong to a specific branch?
The piBuildSessionContext function walks the session's entry graph using the provided leafId as the terminal node. It traverses parent references backward to the session root, collecting only entries that form the direct ancestry chain of the target leaf, while respecting compaction and branching rules defined in the @earendil-works/pi-coding-agent SDK.
What types of session entries are excluded from the messages array?
The entryToUiMessage function filters out entries with types other than "message", such as compaction markers, branch summaries, and custom metadata entries. These internal entries are used for session management but are not displayed in the UI conversation view, ensuring only actual message content reaches the client.
Why does the client request deferThinking and deferMedia parameters?
The deferThinking and deferMedia flags allow the client to request lightweight message payloads. When deferThinking is true, the server strips internal reasoning blocks from assistant messages. When deferMedia is true, entryToUiMessage calls omitToolResultBase64Images to exclude base64-encoded images from tool results, reducing payload size for initial page loads.
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 →