7d60a1af85
- Updated documentation to reflect product name change from 'pi' to 'omp' throughout guides and API references. - Restructured extension and hook documentation to clarify discovery mechanisms, loading behavior, and configuration across multiple config systems (.omp, .pi, .claude, .codex). - Updated SDK API documentation with new method signatures: discoverHooks() -> discoverExtensions(), SessionManager methods now async, settings format changed to YAML. - Expanded session architecture documentation with new entry types (TtsrInjectionEntry, SessionInitEntry), updated field names (fromHook -> fromExtension), and clarified session file format versioning. - Simplified session-tree-plan.md from detailed implementation checklist to architecture summary, removing completed tasks and rollout details.
6.6 KiB
6.6 KiB
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 /branch
| Feature | /branch |
/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_branch / session_branch |
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..."
│ │ └─ • [label-name] user: "That worked..."
│ └─ user: "Actually, approach B..."
│ └─ assistant: "For approach B..."
Controls
| Key | Action |
|---|---|
| ↑/↓ | Move selection |
| ←/→ | Page up/down |
| Enter | Select node |
| Escape | Clear search (if active) or cancel |
| Ctrl+C | Cancel |
| Ctrl+O / Shift+Ctrl+O | Cycle filter forward/back |
| Alt+D/T/U/L/A | Set filter: default / no-tools / user-only / labeled-only / all |
| Shift+L | Edit label for selected entry |
| Type | Search (space-separated tokens) |
| Backspace | Remove last search character |
Display
- Tree list height:
max(5, floor(terminalHeight / 2))lines - Active path marked with a bullet (
•) before each entry (current leaf is last node on the path) - Labels shown inline:
[label-name]before the entry text - Default filter hides
label,custom,model_change, andthinking_level_changeentries - Assistant messages with only tool calls are hidden unless they contain errors/aborts (current leaf is always shown)
no-toolsfilter hides tool result messages- 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):
- Leaf reset to
null(empty conversation) - Message text placed in editor
- User effectively restarts from scratch
Branch Summarization
If branch summaries are enabled (branchSummary.enabled), the user is prompted:
- No summary
- Summarize
- Summarize with custom prompt (passed as
customInstructions)
What Gets Summarized
Path from old leaf back to common ancestor with target:
A → B → C → D → E → F ← old leaf
↘ G → H ← target
Abandoned path: D → E → F (summarized)
Summarization stops at the common ancestor only. Compaction and branch summary entries are included; tool results are ignored.
Summary Storage
Stored as BranchSummaryEntry:
interface BranchSummaryEntry {
type: "branch_summary";
id: string;
parentId: string | null; // New leaf position (null when navigating to root)
timestamp: string;
fromId: string; // Entry the summary is attached to ("root" if null)
summary: string; // LLM-generated summary
details?: unknown; // Optional hook data
fromExtension?: boolean;
}
Implementation
AgentSession.navigateTree()
async navigateTree(
targetId: string,
options?: { summarize?: boolean; customInstructions?: string }
): Promise<{ editorText?: string; cancelled: boolean; aborted?: boolean; summaryEntry?: BranchSummaryEntry }>
Flow:
- Validate target, check no-op (target === current leaf)
- Find common ancestor between old leaf and target
- Collect entries to summarize (if requested, includes compaction entries)
- Fire
session_before_treeevent (hook can cancel or provide summary) - Run default summarizer if needed (respects
customInstructions) - Switch leaf via
branch()orbranchWithSummary() - Update agent:
agent.replaceMessages(sessionManager.buildSessionContext().messages) - Fire
session_treeevent (includessummaryEntry/fromExtensionwhen applicable) - Return result with
editorTextif user message was selected
SessionManager
getLeafId(): 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: string | null, summary, details?, fromExtension?)- Change leaf and create summary entry
InteractiveMode
/tree command shows TreeSelectorComponent, then:
- If
branchSummary.enabled, prompt for summary type (including custom prompt) - Call
session.navigateTree()withsummarize/customInstructions - Clear and re-render chat
- Set editor text if applicable
Hook Events
session_before_tree
interface TreePreparation {
targetId: string;
oldLeafId: string | null;
commonAncestorId: string | null;
entriesToSummarize: SessionEntry[];
userWantsSummary: boolean;
}
interface SessionBeforeTreeEvent {
type: "session_before_tree";
preparation: TreePreparation;
signal: AbortSignal;
}
interface SessionBeforeTreeResult {
cancel?: boolean;
summary?: { summary: string; details?: unknown };
}
session_tree
interface SessionTreeEvent {
type: "session_tree";
newLeafId: string | null;
oldLeafId: string | null;
summaryEntry?: BranchSummaryEntry;
fromExtension?: 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: navigation is cancelled and the caller shows the error
- Escape during summarization: returns
{ cancelled: true, aborted: true }and the selector reopens - Hook returns
cancel: true: navigation is cancelled (caller decides UI) - Escape in the tree selector clears search first, then cancels if empty