clanker-agent/packages/coding-agent/companion-out/docs/tree.md
2026-03-11 02:31:07 -04:00

229 lines
6.9 KiB
Markdown

# 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 `label` and `custom` entries (shown in Ctrl+O mode)
- Children sorted by timestamp (oldest first)
## Selection Behavior
### User Message or Custom Message
1. Leaf set to **parent** of selected node (or `null` if root)
2. Message text placed in **editor** for re-submission
3. User edits and submits, creating a new branch
### Non-User Message (assistant, compaction, etc.)
1. Leaf set to **selected node**
2. Editor stays empty
3. 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:
1. **No summary** - Switch immediately without summarizing
2. **Summarize** - Generate a summary using the default prompt
3. **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:
```
A → B → C → D → E → F ← old leaf
↘ G → H ← 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`:
```typescript
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()
```typescript
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 timestamp
- `branch(id)` - Change leaf pointer
- `branchWithSummary(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`
```typescript
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`
```typescript
interface SessionTreeEvent {
type: "session_tree";
newLeafId: string | null;
oldLeafId: string | null;
summaryEntry?: BranchSummaryEntry;
fromHook?: boolean;
}
```
### Example: Custom Summarizer
```typescript
export default function (companion: HookAPI) {
companion.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