mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-15 17:00:58 +00:00
Complete the remaining pi-to-companion rename across companion-os, web, vm-orchestrator, docker, and archived fixtures. Verification: - semantic rg sweeps for Pi/piConfig/getPi/.pi runtime references - npm run check in apps/companion-os (fails in this worktree: biome not found) Co-authored-by: Codex <noreply@openai.com>
229 lines
6.9 KiB
Markdown
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
|