Session Tree Navigation¶
The /tree command provides tree-based navigation of the session history.
Overview¶
Sessions are stored as trees where each entry has an id and parentId. The "leaf" pointer tracks the current position. /tree lets you navigate to any point and optionally summarize the branch you're leaving.
Comparison with /fork¶
| Feature | /fork |
/tree |
|---|---|---|
| View | Flat list of user messages | Full tree structure |
| Action | Extracts path to new session file | Changes leaf in same session |
| Summary | Never | Optional (user prompted) |
| Events | session_before_fork / session_fork |
session_before_tree / session_tree |
Tree UI¶
├─ user: "Hello, can you help..."
│ └─ assistant: "Of course! I can..."
│ ├─ user: "Let's try approach A..."
│ │ └─ assistant: "For approach A..."
│ │ └─ [compaction: 12k tokens]
│ │ └─ user: "That worked..." ← active
│ └─ user: "Actually, approach B..."
│ └─ assistant: "For approach B..."
Controls¶
| Key | Action |
|---|---|
| ↑/↓ | Navigate (depth-first order) |
| Enter | Select node |
| Escape/Ctrl+C | Cancel |
| Ctrl+U | Toggle: user messages only |
| Ctrl+O | Toggle: show all (including custom/label entries) |
Display¶
- Height: half terminal height
- Current leaf marked with
← active - Labels shown inline:
[label-name] - Default filter hides
labelandcustomentries (shown in Ctrl+O mode) - Children sorted by timestamp (oldest first)
Selection Behavior¶
User Message or Custom Message¶
- Leaf set to parent of selected node (or
nullif root) - Message text placed in editor for re-submission
- User edits and submits, creating a new branch
Non-User Message (assistant, compaction, etc.)¶
- Leaf set to selected node
- Editor stays empty
- User continues from that point
Selecting Root User Message¶
If user selects the very first message (has no parent):
1. Leaf reset to null (empty conversation)
2. Message text placed in editor
3. User effectively restarts from scratch
Branch Summarization¶
When switching branches, user is presented with three options:
- No summary - Switch immediately without summarizing
- Summarize - Generate a summary using the default prompt
- Summarize with custom prompt - Opens an editor to enter additional focus instructions that are appended to the default summarization prompt
What Gets Summarized¶
Path from old leaf back to common ancestor with target:
Abandoned path: D → E → F (summarized)
Summarization stops at: 1. Common ancestor (always) 2. Compaction node (if encountered first)
Summary Storage¶
Stored as BranchSummaryEntry:
interface BranchSummaryEntry {
type: "branch_summary";
id: string;
parentId: string; // New leaf position
timestamp: string;
fromId: string; // Old leaf we abandoned
summary: string; // LLM-generated summary
details?: unknown; // Optional hook data
}
Implementation¶
AgentSession.navigateTree()¶
async navigateTree(
targetId: string,
options?: {
summarize?: boolean;
customInstructions?: string;
replaceInstructions?: boolean;
label?: string;
}
): Promise<{ editorText?: string; cancelled: boolean }>
Options:
- summarize: Whether to generate a summary of the abandoned branch
- customInstructions: Custom instructions for the summarizer
- replaceInstructions: If true, customInstructions replaces the default prompt instead of being appended
- label: Label to attach to the branch summary entry (or target entry if not summarizing)
Flow:
1. Validate target, check no-op (target === current leaf)
2. Find common ancestor between old leaf and target
3. Collect entries to summarize (if requested)
4. Fire session_before_tree event (hook can cancel or provide summary)
5. Run default summarizer if needed
6. Switch leaf via branch() or branchWithSummary()
7. Update agent: agent.replaceMessages(sessionManager.buildSessionContext().messages)
8. Fire session_tree event
9. Notify custom tools via session event
10. Return result with editorText if user message was selected
SessionManager¶
getLeafUuid(): string | null- Current leaf (null if empty)resetLeaf(): void- Set leaf to null (for root user message navigation)getTree(): SessionTreeNode[]- Full tree with children sorted by timestampbranch(id)- Change leaf pointerbranchWithSummary(id, summary)- Change leaf and create summary entry
InteractiveMode¶
/tree command shows TreeSelectorComponent, then:
1. Prompt for summarization
2. Call session.navigateTree()
3. Clear and re-render chat
4. Set editor text if applicable
Hook Events¶
session_before_tree¶
interface TreePreparation {
targetId: string;
oldLeafId: string | null;
commonAncestorId: string | null;
entriesToSummarize: SessionEntry[];
userWantsSummary: boolean;
customInstructions?: string;
replaceInstructions?: boolean;
label?: string;
}
interface SessionBeforeTreeEvent {
type: "session_before_tree";
preparation: TreePreparation;
signal: AbortSignal;
}
interface SessionBeforeTreeResult {
cancel?: boolean;
summary?: { summary: string; details?: unknown };
customInstructions?: string; // Override custom instructions
replaceInstructions?: boolean; // Override replace mode
label?: string; // Override label
}
Extensions can override customInstructions, replaceInstructions, and label by returning them from the session_before_tree handler.
session_tree¶
interface SessionTreeEvent {
type: "session_tree";
newLeafId: string | null;
oldLeafId: string | null;
summaryEntry?: BranchSummaryEntry;
fromHook?: boolean;
}
Example: Custom Summarizer¶
export default function(pi: HookAPI) {
pi.on("session_before_tree", async (event, ctx) => {
if (!event.preparation.userWantsSummary) return;
if (event.preparation.entriesToSummarize.length === 0) return;
const summary = await myCustomSummarizer(event.preparation.entriesToSummarize);
return { summary: { summary, details: { custom: true } } };
});
}
Error Handling¶
- Summarization failure: cancels navigation, shows error
- User abort (Escape): cancels navigation
- Hook returns
cancel: true: cancels navigation silently