# Branch Summary This document describes the `/tree` command and branch summarization feature. ## Overview The `/tree` command provides tree-based navigation of the session history, allowing users to: 1. View the entire session tree structure 2. Switch to any branch point 3. Optionally summarize the branch being abandoned This differs from `/branch` which extracts a linear path to a new session file. ## Commands ### `/branch` (existing) - Shows a flat list of user messages - Extracts selected path to a **new session file** - Selected user message text goes to editor for re-submission - Fires `session_before_branch` / `session_branch` events ### `/tree` (new) - Shows the **full session tree** with visual hierarchy - Navigates within the **same session file** (changes active leaf) - Optionally summarizes the abandoned branch - Fires `session_before_tree` / `session_tree` events ## Tree UI The tree selector displays the session structure with ASCII art: ``` ├─ 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, now..." │ │ └─ assistant: "Great! Next..." ← active │ └─ user: "Actually, approach B..." │ └─ assistant: "For approach B..." ``` ### Visual Indicators | Element | Display | |---------|---------| | Current active leaf | `← active` suffix, highlighted | | User messages | Normal color (selectable) | | Custom messages (display: true) | Normal color (selectable) | | Assistant/tool results | Dimmed (selectable, for context continuation) | | Compaction nodes | `[compaction: Xk tokens]` | | Branch points | Node with multiple children visible | ### Navigation | Key | Action | |-----|--------| | ↑/↓ | Move through nodes (depth-first pre-order) | | Enter | Select node and proceed | | Escape | Cancel | | Ctrl+C | Cancel | | Ctrl+U | Toggle: show only user messages | | Ctrl+O | Toggle: show all entries (including custom/label) | ### Filtering Default view hides: - `label` entries (labels shown inline on their target node) - `custom` entries (hook state, not relevant for navigation) Ctrl+O shows everything for debugging/inspection. ### Component Size Height is capped at **half terminal height** to show substantial tree context without overshooting the terminal. ## Selection Behavior ### Selecting Current Active Leaf No-op. Display message: "Already at this point." ### Switching to Different Node ### User Message or Custom Message Selected 1. Active leaf is set to **parent** of selected node 2. Selected message text is placed in the **editor** for re-submission 3. User edits and submits, creating a new branch from that point ### Non-User Message Selected (assistant, tool result, etc.) 1. Active leaf is set to the **selected node itself** 2. Editor remains empty 3. User continues the conversation from that point ## Branch Summarization When switching branches, the user is prompted: "Summarize the branch you're leaving?" ### What Gets Summarized The abandoned branch is the path from the **old active leaf** back to the **common ancestor** of the old leaf and newly selected node. ``` A → B → C → D → E → F ← old active leaf ↘ G → H ← user selects H ``` - Common ancestor: C - Abandoned path: D → E → F - These nodes are summarized ### Stopping Conditions When walking back from the old leaf to gather content for summarization: 1. **Stop at common ancestor** (always) 2. **Stop at compaction node** (if encountered before common ancestor) - Compaction already summarizes older content - Only summarize "fresh" content after the compaction ### Summary Storage The summary is stored as a `BranchSummaryEntry`: ```typescript interface BranchSummaryEntry { type: "branch_summary"; id: string; parentId: string; // Points to common ancestor timestamp: string; fromId: string; // The old leaf we abandoned summary: string; // LLM-generated summary details?: unknown; // Optional hook data } ``` The summary entry becomes a sibling of the path we're switching to, preserving the record of what was abandoned. ### Summary Generation The summarizer: 1. Collects messages from old leaf back to stopping point 2. Sends to LLM with prompt: "Summarize this conversation branch concisely" 3. Creates `BranchSummaryEntry` with the result User can skip summarization, in which case no `BranchSummaryEntry` is created. ## Example Flow ``` Initial state: A → B → C → D ← active User runs /tree, selects B: 1. Show tree: ├─ A (user): "Start task..." │ └─ B (assistant): "I'll help..." │ └─ C (user): "Do X..." │ └─ D (assistant): "Done X..." ← active 2. User navigates to B, presses Enter 3. Prompt: "Summarize branch you're leaving? [Y/n]" 4a. If Yes: - Summarize C → D - Create BranchSummaryEntry(fromId: D, summary: "...") - Set active leaf to B - Tree becomes: A → B → C → D ↓ ↘ [summary: "Tried X..."] └─ (active, user continues from here) 4b. If No: - Set active leaf to B - No summary entry created 5. Since B is assistant message: - Editor stays empty - User types new message, branches from B ``` ## Implementation Notes ### SessionManager Methods (already exist) - `getTree()` - Get full tree structure for display (needs: sort children by timestamp) - `getPath(id)` - Get path from root to any node - `getEntry(id)` - Look up individual entries - `getLeafUuid()` - Get current active leaf - `branch(id)` - Change active leaf - `branchWithSummary(fromId, summary)` - Create branch summary entry - `buildSessionContext()` - Get messages for LLM from current leaf ### AgentSession: New `navigateTree()` Method ```typescript interface NavigateTreeOptions { /** Whether user wants to summarize abandoned branch */ summarize?: boolean; /** Custom instructions for summarizer */ customInstructions?: string; } interface NavigateTreeResult { /** Text to put in editor (if user message selected) */ editorText?: string; /** Whether navigation was cancelled */ cancelled: boolean; } async navigateTree(targetId: string, options?: NavigateTreeOptions): Promise ``` Implementation flow: 1. **Validate target exists** 2. **Check if no-op** (target === current leaf) → return early 3. **Prepare summarization** (if `options.summarize`): - Find common ancestor - Collect entries to summarize (old leaf → common ancestor, stop at compaction) 4. **Fire `session_before_tree` event**: - Pass preparation, model, signal - If hook returns `cancel: true` → return `{ cancelled: true }` - If hook returns custom summary → use it, skip default summarizer 5. **Run default summarizer** (if needed): - Use conversation model - On failure/abort → return `{ cancelled: true }` 6. **Switch leaf**: - If summarizing: `sessionManager.branchWithSummary(targetId, summary)` - Otherwise: `sessionManager.branch(targetId)` 7. **Update agent state**: ```typescript const context = this.sessionManager.buildSessionContext(); this.agent.replaceMessages(context.messages); ``` 8. **Fire `session_tree` event** 9. **Notify custom tools** via `_emitToolSessionEvent("tree", ...)` 10. **Return result**: - If target was user message: `{ editorText: messageText, cancelled: false }` - Otherwise: `{ cancelled: false }` ### InteractiveMode: `/tree` Command Handler ```typescript if (text === "/tree") { this.showTreeSelector(); this.editor.setText(""); return; } ``` `showTreeSelector()` flow: 1. Get tree via `sessionManager.getTree()` 2. Show `TreeSelectorComponent` (new component) 3. On selection: - If target === current leaf → show "Already at this point", done - Prompt: "Summarize branch you're leaving? [Y/n]" - Call `session.navigateTree(targetId, { summarize })` - If cancelled → done - Clear chat: `this.chatContainer.clear()` - Re-render: `this.renderInitialMessages()` - If `result.editorText` → `this.editor.setText(result.editorText)` - Show status: "Switched to entry X" ### TUI Update Flow After `navigateTree()` completes successfully: ```typescript // In InteractiveMode, after navigateTree returns if (!result.cancelled) { this.chatContainer.clear(); this.renderInitialMessages(); // Uses sessionManager.buildSessionContext() if (result.editorText) { this.editor.setText(result.editorText); } this.showStatus("Navigated to selected point"); } ``` This matches the existing pattern in `handleResumeSession()` and `handleClearCommand()`. ### Finding Common Ancestor ```typescript function findCommonAncestor(nodeA: string, nodeB: string): string { const pathA = new Set(sessionManager.getPath(nodeA).map(e => e.id)); for (const entry of sessionManager.getPath(nodeB)) { if (pathA.has(entry.id)) { return entry.id; } } throw new Error("No common ancestor found"); } ``` ### Collecting Abandoned Branch ```typescript function collectAbandonedBranch(oldLeaf: string, commonAncestor: string): SessionEntry[] { const entries: SessionEntry[] = []; let current = oldLeaf; while (current !== commonAncestor) { const entry = sessionManager.getEntry(current); if (!entry) break; // Stop at compaction - older content already summarized if (entry.type === "compaction") break; entries.push(entry); current = entry.parentId; } return entries.reverse(); // Chronological order } ``` ### Tree Child Ordering `getTree()` should sort children by timestamp (oldest first, newest at bottom): ```typescript // In getTree(), after building tree: function sortChildren(node: SessionTreeNode): void { node.children.sort((a, b) => new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime() ); node.children.forEach(sortChildren); } roots.forEach(sortChildren); ``` ### Error Handling **Summarization fails** (API error, timeout, etc.): - Cancel the entire switch - Show error message - User stays at current position **User aborts during summarization** (Escape): - Cancel the entire switch - Show "Navigation cancelled" - User stays at current position **Hook returns `cancel: true`**: - Cancel the switch - No error message (hook may have shown its own UI) - User stays at current position ### TreeSelectorComponent New TUI component at `src/modes/interactive/components/tree-selector.ts`: ```typescript interface TreeSelectorProps { tree: SessionTreeNode[]; currentLeafId: string; onSelect: (entryId: string) => void; onCancel: () => void; } ``` Features: - Height: half terminal height (capped) - ASCII tree rendering with `├─`, `│`, `└─` connectors - Depth-first traversal for up/down navigation - Visual indicators: - `← active` for current leaf - Resolved labels shown inline - Compaction nodes as `[compaction: Xk tokens]` - Filter modes: - Default: hide `label` and `custom` entries - Ctrl+U: user messages only - Ctrl+O: show all entries - Scrolling with selected node kept visible ## Hook Events These events are separate from `session_before_branch`/`session_branch` which are used by the existing `/branch` command (creates new session file). ### `session_before_tree` Fired before switching branches within the same session file. Hooks can cancel or provide custom summary. ```typescript interface TreePreparation { /** Node being switched to */ targetId: string; /** Current active leaf (being abandoned) */ oldLeafId: string; /** Common ancestor of target and old leaf */ commonAncestorId: string; /** Entries to summarize (old leaf back to common ancestor or compaction) */ entriesToSummarize: SessionEntry[]; /** Whether user chose to summarize */ userWantsSummary: boolean; } interface SessionBeforeTreeEvent { type: "session_before_tree"; preparation: TreePreparation; /** Model to use for summarization (conversation model) */ model: Model; /** Abort signal - honors Escape during summarization */ signal: AbortSignal; } interface SessionBeforeTreeResult { /** Cancel the navigation entirely */ cancel?: boolean; /** Custom summary (skips default summarizer). Only used if userWantsSummary is true. */ summary?: { summary: string; details?: unknown; }; } ``` ### `session_tree` Fired after navigation completes successfully. Not fired if cancelled. ```typescript interface SessionTreeEvent { type: "session_tree"; /** The new active leaf */ newLeafId: string; /** Previous active leaf */ oldLeafId: string; /** Branch summary entry if one was created, undefined if user skipped summarization */ summaryEntry?: BranchSummaryEntry; /** Whether summary came from hook (false if default summarizer used, undefined if no summary) */ fromHook?: boolean; } ``` ### Example: Custom Branch Summarizer ```typescript 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; // Use a different model for summarization const model = getModel("google", "gemini-2.5-flash"); const apiKey = await ctx.modelRegistry.getApiKey(model); // Custom summarization logic const summary = await summarizeWithCustomPrompt( event.preparation.entriesToSummarize, model, apiKey ); return { summary: { summary, details: { model: model.id, timestamp: Date.now() } } }; }); } ```