Branching Conversation Navigation#
The conversation-flow package maintains messages as a tree (each message has a parentId), but the UI renders a single linear list. Branching navigation is the mechanism by which one active path is selected from that tree and projected into a flat, ordered sequence.
Two classes collaborate to do this:
BranchResolver— pure logic for picking which child is the "active branch" at any fork.FlatListBuilder— recursive tree traversal that consumesBranchResolverto produce the flat rendering list.
A parallel output, ContextTreeBuilder, uses BranchResolver to produce a semantic context tree (with explicit BranchNode entries) used for navigation UI rather than rendering.
Both builders are wired together by the Transformer class in transformation/index.ts and invoked from the top-level parse.ts entry point.
The activeBranchIndex Metadata Field#
metadata.activeBranchIndex is the primary signal stored on a parent message to declare which of its children is the currently visible branch. It is a zero-based integer index into the ordered list of child IDs.
BranchResolver applies a three-priority fallback strategy when resolving the active branch :
| Priority | Strategy |
|---|---|
| 1 | Read message.metadata.activeBranchIndex; use if in-bounds |
| 2 | Infer: pick the first child that itself has descendants |
| 3 | Default to the first child (childIds[0]) |
A special optimistic-update case: when activeBranchIndex === childIds.length, the branch is being created but doesn't exist yet — BranchResolver returns undefined and the caller omits branch navigation UI .
BranchResolver exposes two method variants with identical logic:
getActiveBranchId(message, idNode)— used byContextTreeBuilder, operates on theIdNodetree structure.getActiveBranchIdFromMetadata(message, childIds, childrenMap)— used byFlatListBuilder, operates on flatchildIdsarrays and achildrenMap.
How FlatListBuilder Selects the Active Path#
FlatListBuilder.flatten() starts from the root (messages with no parentId) and calls buildFlatListRecursive(), which walks the tree depth-first. At each fork it calls BranchResolver to pick one child and recurses only into that branch.
Two branching cases are handled:
User message with multiple assistant children : BranchResolver.getActiveBranchIdFromMetadata selects the active assistant. The selected message is emitted with a branch annotation { count, activeBranchIndex } attached, so the UI can render branch-switching controls.
Assistant message with multiple user children : Same pattern — BranchResolver picks the active user reply, which is emitted with branch metadata.
In both cases, tool-message children are excluded from branch counting because they are inline data of their parent assistant, not sibling branches .
How ContextTreeBuilder Produces a BranchNode#
ContextTreeBuilder.createBranchNode() calls BranchResolver.getActiveBranchId, then:
- Records the resolved
activeBranchIndexon theBranchNode. - Recursively builds all branches as full subtrees (not just the active one) — all branches exist in the context tree; only
activeBranchIndexflags which is currently selected.
This gives navigation UI a complete picture of every available path while still marking one as active.
Key Files#
| File | Role |
|---|---|
BranchResolver.ts | Branch selection logic (three-priority strategy + optimistic updates) |
FlatListBuilder.ts | Flat rendering list; consumes BranchResolver at branching points |
ContextTreeBuilder.ts | Semantic context tree; emits BranchNode with full branch subtrees |
types/contextTree.ts | BranchNode type definition (activeBranchIndex, branches) |
transformation/index.ts | Wires all builders together via Transformer |
parse.ts | Top-level entry point; calls transformAll + flatten |