mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 07:03:25 +00:00
Rename /branch command to /fork
- RPC: branch -> fork, get_branch_messages -> get_fork_messages - SDK: branch() -> fork(), getBranchMessages() -> getForkMessages() - AgentSession: branch() -> fork(), getUserMessagesForBranching() -> getUserMessagesForForking() - Extension events: session_before_branch -> session_before_fork, session_branch -> session_fork - Settings: doubleEscapeAction 'branch' -> 'fork' fixes #641
This commit is contained in:
parent
e7352a50bf
commit
df3f5f41c0
27 changed files with 162 additions and 156 deletions
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- Renamed `/branch` command to `/fork` ([#641](https://github.com/badlogic/pi-mono/issues/641))
|
||||||
|
- RPC: `branch` → `fork`, `get_branch_messages` → `get_fork_messages`
|
||||||
|
- SDK: `branch()` → `fork()`, `getBranchMessages()` → `getForkMessages()`
|
||||||
|
- AgentSession: `branch()` → `fork()`, `getUserMessagesForBranching()` → `getUserMessagesForForking()`
|
||||||
|
- Extension events: `session_before_branch` → `session_before_fork`, `session_branch` → `session_fork`
|
||||||
|
- Settings: `doubleEscapeAction: "branch" | "tree"` → `"fork" | "tree"`
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `/models` command to enable/disable models for Ctrl+P cycling. Changes persist to `enabledModels` in settings.json and take effect immediately. ([#626](https://github.com/badlogic/pi-mono/pull/626) by [@CarlosGtrz](https://github.com/CarlosGtrz))
|
- `/models` command to enable/disable models for Ctrl+P cycling. Changes persist to `enabledModels` in settings.json and take effect immediately. ([#626](https://github.com/badlogic/pi-mono/pull/626) by [@CarlosGtrz](https://github.com/CarlosGtrz))
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ The agent reads, writes, and edits files, and executes commands via bash.
|
||||||
| `/hotkeys` | Show all keyboard shortcuts |
|
| `/hotkeys` | Show all keyboard shortcuts |
|
||||||
| `/changelog` | Display full version history |
|
| `/changelog` | Display full version history |
|
||||||
| `/tree` | Navigate session tree in-place (search, filter, label entries) |
|
| `/tree` | Navigate session tree in-place (search, filter, label entries) |
|
||||||
| `/branch` | Create new conversation branch from a previous message |
|
| `/fork` | Create new conversation fork from a previous message |
|
||||||
| `/resume` | Switch to a different session (interactive selector) |
|
| `/resume` | Switch to a different session (interactive selector) |
|
||||||
| `/login` | OAuth login for subscription-based models |
|
| `/login` | OAuth login for subscription-based models |
|
||||||
| `/logout` | Clear OAuth tokens |
|
| `/logout` | Clear OAuth tokens |
|
||||||
|
|
@ -507,10 +507,10 @@ See [docs/compaction.md](docs/compaction.md) for how compaction works internally
|
||||||
- Press `l` to label entries as bookmarks
|
- Press `l` to label entries as bookmarks
|
||||||
- When switching branches, you're prompted whether to generate a summary of the abandoned branch (messages up to the common ancestor)
|
- When switching branches, you're prompted whether to generate a summary of the abandoned branch (messages up to the common ancestor)
|
||||||
|
|
||||||
**Create new session (`/branch`):** Branch to a new session file:
|
**Create new session (`/fork`):** Fork to a new session file:
|
||||||
|
|
||||||
1. Opens selector showing all your user messages
|
1. Opens selector showing all your user messages
|
||||||
2. Select a message to branch from
|
2. Select a message to fork from
|
||||||
3. Creates new session with history up to that point
|
3. Creates new session with history up to that point
|
||||||
4. Selected message placed in editor for modification
|
4. Selected message placed in editor for modification
|
||||||
|
|
||||||
|
|
@ -859,7 +859,7 @@ Extensions are TypeScript modules that extend pi's behavior.
|
||||||
- **Custom tools** - Register tools callable by the LLM with custom UI and rendering
|
- **Custom tools** - Register tools callable by the LLM with custom UI and rendering
|
||||||
- **Custom commands** - Add `/commands` for users (e.g., `/deploy`, `/stats`)
|
- **Custom commands** - Add `/commands` for users (e.g., `/deploy`, `/stats`)
|
||||||
- **Event interception** - Block tool calls, modify results, customize compaction
|
- **Event interception** - Block tool calls, modify results, customize compaction
|
||||||
- **State persistence** - Store data in session, reconstruct on reload/branch
|
- **State persistence** - Store data in session, reconstruct on reload/fork
|
||||||
- **External integrations** - File watchers, webhooks, git checkpointing
|
- **External integrations** - File watchers, webhooks, git checkpointing
|
||||||
- **Custom UI** - Full TUI control from tools, commands, or event handlers
|
- **Custom UI** - Full TUI control from tools, commands, or event handlers
|
||||||
|
|
||||||
|
|
@ -1011,7 +1011,7 @@ export default function (pi: ExtensionAPI) {
|
||||||
};
|
};
|
||||||
|
|
||||||
pi.on("session_start", async (e, ctx) => reconstruct(ctx));
|
pi.on("session_start", async (e, ctx) => reconstruct(ctx));
|
||||||
pi.on("session_branch", async (e, ctx) => reconstruct(ctx));
|
pi.on("session_fork", async (e, ctx) => reconstruct(ctx));
|
||||||
pi.on("session_tree", async (e, ctx) => reconstruct(ctx));
|
pi.on("session_tree", async (e, ctx) => reconstruct(ctx));
|
||||||
|
|
||||||
pi.registerCommand("increment", {
|
pi.registerCommand("increment", {
|
||||||
|
|
|
||||||
|
|
@ -278,9 +278,9 @@ user sends another prompt ◄─────────────────
|
||||||
├─► session_before_switch (can cancel)
|
├─► session_before_switch (can cancel)
|
||||||
└─► session_switch
|
└─► session_switch
|
||||||
|
|
||||||
/branch
|
/fork
|
||||||
├─► session_before_branch (can cancel)
|
├─► session_before_fork (can cancel)
|
||||||
└─► session_branch
|
└─► session_fork
|
||||||
|
|
||||||
/compact or auto-compaction
|
/compact or auto-compaction
|
||||||
├─► session_before_compact (can cancel or customize)
|
├─► session_before_compact (can cancel or customize)
|
||||||
|
|
@ -334,19 +334,19 @@ pi.on("session_switch", async (event, ctx) => {
|
||||||
|
|
||||||
**Examples:** [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [status-line.ts](../examples/extensions/status-line.ts), [todo.ts](../examples/extensions/todo.ts)
|
**Examples:** [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [status-line.ts](../examples/extensions/status-line.ts), [todo.ts](../examples/extensions/todo.ts)
|
||||||
|
|
||||||
#### session_before_branch / session_branch
|
#### session_before_fork / session_fork
|
||||||
|
|
||||||
Fired when branching via `/branch`.
|
Fired when forking via `/fork`.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
pi.on("session_before_branch", async (event, ctx) => {
|
pi.on("session_before_fork", async (event, ctx) => {
|
||||||
// event.entryId - ID of the entry being branched from
|
// event.entryId - ID of the entry being forked from
|
||||||
return { cancel: true }; // Cancel branch
|
return { cancel: true }; // Cancel fork
|
||||||
// OR
|
// OR
|
||||||
return { skipConversationRestore: true }; // Branch but don't rewind messages
|
return { skipConversationRestore: true }; // Fork but don't rewind messages
|
||||||
});
|
});
|
||||||
|
|
||||||
pi.on("session_branch", async (event, ctx) => {
|
pi.on("session_fork", async (event, ctx) => {
|
||||||
// event.previousSessionFile - previous session file
|
// event.previousSessionFile - previous session file
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -541,47 +541,47 @@ If a hook cancelled the switch:
|
||||||
{"type": "response", "command": "switch_session", "success": true, "data": {"cancelled": true}}
|
{"type": "response", "command": "switch_session", "success": true, "data": {"cancelled": true}}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### branch
|
#### fork
|
||||||
|
|
||||||
Create a new branch from a previous user message. Can be cancelled by a `before_branch` hook. Returns the text of the message being branched from.
|
Create a new fork from a previous user message. Can be cancelled by a `before_fork` hook. Returns the text of the message being forked from.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"type": "branch", "entryId": "abc123"}
|
{"type": "fork", "entryId": "abc123"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Response:
|
Response:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "response",
|
"type": "response",
|
||||||
"command": "branch",
|
"command": "fork",
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": {"text": "The original prompt text...", "cancelled": false}
|
"data": {"text": "The original prompt text...", "cancelled": false}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If a hook cancelled the branch:
|
If a hook cancelled the fork:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "response",
|
"type": "response",
|
||||||
"command": "branch",
|
"command": "fork",
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": {"text": "The original prompt text...", "cancelled": true}
|
"data": {"text": "The original prompt text...", "cancelled": true}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### get_branch_messages
|
#### get_fork_messages
|
||||||
|
|
||||||
Get user messages available for branching.
|
Get user messages available for forking.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"type": "get_branch_messages"}
|
{"type": "get_fork_messages"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Response:
|
Response:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "response",
|
"type": "response",
|
||||||
"command": "get_branch_messages",
|
"command": "get_fork_messages",
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": {
|
"data": {
|
||||||
"messages": [
|
"messages": [
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,8 @@ interface AgentSession {
|
||||||
newSession(options?: { parentSession?: string }): Promise<boolean>; // Returns false if cancelled by hook
|
newSession(options?: { parentSession?: string }): Promise<boolean>; // Returns false if cancelled by hook
|
||||||
switchSession(sessionPath: string): Promise<boolean>;
|
switchSession(sessionPath: string): Promise<boolean>;
|
||||||
|
|
||||||
// Branching
|
// Forking
|
||||||
branch(entryId: string): Promise<{ selectedText: string; cancelled: boolean }>; // Creates new session file
|
fork(entryId: string): Promise<{ selectedText: string; cancelled: boolean }>; // Creates new session file
|
||||||
navigateTree(targetId: string, options?: { summarize?: boolean }): Promise<{ editorText?: string; cancelled: boolean }>; // In-place navigation
|
navigateTree(targetId: string, options?: { summarize?: boolean }): Promise<{ editorText?: string; cancelled: boolean }>; // In-place navigation
|
||||||
|
|
||||||
// Hook message injection
|
// Hook message injection
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ First line of the file. Metadata only, not part of the tree (no `id`/`parentId`)
|
||||||
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}
|
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}
|
||||||
```
|
```
|
||||||
|
|
||||||
For sessions with a parent (created via `/branch` or `newSession({ parentSession })`):
|
For sessions with a parent (created via `/fork` or `newSession({ parentSession })`):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}
|
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ The `/tree` command provides tree-based navigation of the session history.
|
||||||
|
|
||||||
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.
|
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`
|
### Comparison with `/fork`
|
||||||
|
|
||||||
| Feature | `/branch` | `/tree` |
|
| Feature | `/fork` | `/tree` |
|
||||||
|---------|-----------|---------|
|
|---------|---------|---------|
|
||||||
| View | Flat list of user messages | Full tree structure |
|
| View | Flat list of user messages | Full tree structure |
|
||||||
| Action | Extracts path to **new session file** | Changes leaf in **same session** |
|
| Action | Extracts path to **new session file** | Changes leaf in **same session** |
|
||||||
| Summary | Never | Optional (user prompted) |
|
| Summary | Never | Optional (user prompted) |
|
||||||
| Events | `session_before_branch` / `session_branch` | `session_before_tree` / `session_tree` |
|
| Events | `session_before_fork` / `session_fork` | `session_before_tree` / `session_tree` |
|
||||||
|
|
||||||
## Tree UI
|
## Tree UI
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
| `permission-gate.ts` | Prompts for confirmation before dangerous bash commands (rm -rf, sudo, etc.) |
|
| `permission-gate.ts` | Prompts for confirmation before dangerous bash commands (rm -rf, sudo, etc.) |
|
||||||
| `protected-paths.ts` | Blocks writes to protected paths (.env, .git/, node_modules/) |
|
| `protected-paths.ts` | Blocks writes to protected paths (.env, .git/, node_modules/) |
|
||||||
| `confirm-destructive.ts` | Confirms before destructive session actions (clear, switch, branch) |
|
| `confirm-destructive.ts` | Confirms before destructive session actions (clear, switch, fork) |
|
||||||
| `dirty-repo-guard.ts` | Prevents session changes with uncommitted git changes |
|
| `dirty-repo-guard.ts` | Prevents session changes with uncommitted git changes |
|
||||||
|
|
||||||
### Custom Tools
|
### Custom Tools
|
||||||
|
|
@ -53,7 +53,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
||||||
|
|
||||||
| Extension | Description |
|
| Extension | Description |
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
| `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on branch |
|
| `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on fork |
|
||||||
| `auto-commit-on-exit.ts` | Auto-commits on exit using last assistant message for commit message |
|
| `auto-commit-on-exit.ts` | Auto-commits on exit using last assistant message for commit message |
|
||||||
|
|
||||||
### System Prompt & Compaction
|
### System Prompt & Compaction
|
||||||
|
|
@ -129,7 +129,7 @@ action: Type.Union([Type.Literal("list"), Type.Literal("add")])
|
||||||
|
|
||||||
**State persistence via details:**
|
**State persistence via details:**
|
||||||
```typescript
|
```typescript
|
||||||
// Store state in tool result details for proper branching support
|
// Store state in tool result details for proper forking support
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: "Done" }],
|
content: [{ type: "text", text: "Done" }],
|
||||||
details: { todos: [...todos], nextId }, // Persisted in session
|
details: { todos: [...todos], nextId }, // Persisted in session
|
||||||
|
|
|
||||||
|
|
@ -43,16 +43,16 @@ export default function (pi: ExtensionAPI) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
pi.on("session_before_branch", async (event, ctx) => {
|
pi.on("session_before_fork", async (event, ctx) => {
|
||||||
if (!ctx.hasUI) return;
|
if (!ctx.hasUI) return;
|
||||||
|
|
||||||
const choice = await ctx.ui.select(`Branch from entry ${event.entryId.slice(0, 8)}?`, [
|
const choice = await ctx.ui.select(`Fork from entry ${event.entryId.slice(0, 8)}?`, [
|
||||||
"Yes, create branch",
|
"Yes, create fork",
|
||||||
"No, stay in current session",
|
"No, stay in current session",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (choice !== "Yes, create branch") {
|
if (choice !== "Yes, create fork") {
|
||||||
ctx.ui.notify("Branch cancelled", "info");
|
ctx.ui.notify("Fork cancelled", "info");
|
||||||
return { cancel: true };
|
return { cancel: true };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export default function (pi: ExtensionAPI) {
|
||||||
return checkDirtyRepo(pi, ctx, action);
|
return checkDirtyRepo(pi, ctx, action);
|
||||||
});
|
});
|
||||||
|
|
||||||
pi.on("session_before_branch", async (_event, ctx) => {
|
pi.on("session_before_fork", async (_event, ctx) => {
|
||||||
return checkDirtyRepo(pi, ctx, "branch");
|
return checkDirtyRepo(pi, ctx, "fork");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Git Checkpoint Extension
|
* Git Checkpoint Extension
|
||||||
*
|
*
|
||||||
* Creates git stash checkpoints at each turn so /branch can restore code state.
|
* Creates git stash checkpoints at each turn so /fork can restore code state.
|
||||||
* When branching, offers to restore code to that point in history.
|
* When forking, offers to restore code to that point in history.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
|
@ -26,7 +26,7 @@ export default function (pi: ExtensionAPI) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
pi.on("session_before_branch", async (event, ctx) => {
|
pi.on("session_before_fork", async (event, ctx) => {
|
||||||
const ref = checkpoints.get(event.entryId);
|
const ref = checkpoints.get(event.entryId);
|
||||||
if (!ref) return;
|
if (!ref) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ export default function (pi: ExtensionAPI) {
|
||||||
// Reconstruct state on session events
|
// Reconstruct state on session events
|
||||||
pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
|
pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
|
||||||
pi.on("session_switch", async (_event, ctx) => reconstructState(ctx));
|
pi.on("session_switch", async (_event, ctx) => reconstructState(ctx));
|
||||||
pi.on("session_branch", async (_event, ctx) => reconstructState(ctx));
|
pi.on("session_fork", async (_event, ctx) => reconstructState(ctx));
|
||||||
pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
|
pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
|
||||||
|
|
||||||
// Register the todo tool for the LLM
|
// Register the todo tool for the LLM
|
||||||
|
|
|
||||||
|
|
@ -138,8 +138,8 @@ export default function toolsExtension(pi: ExtensionAPI) {
|
||||||
restoreFromBranch(ctx);
|
restoreFromBranch(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restore state after branching
|
// Restore state after forking
|
||||||
pi.on("session_branch", async (_event, ctx) => {
|
pi.on("session_fork", async (_event, ctx) => {
|
||||||
restoreFromBranch(ctx);
|
restoreFromBranch(ctx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ import {
|
||||||
import { exportSessionToHtml } from "./export-html/index.js";
|
import { exportSessionToHtml } from "./export-html/index.js";
|
||||||
import type {
|
import type {
|
||||||
ExtensionRunner,
|
ExtensionRunner,
|
||||||
SessionBeforeBranchResult,
|
|
||||||
SessionBeforeCompactResult,
|
SessionBeforeCompactResult,
|
||||||
|
SessionBeforeForkResult,
|
||||||
SessionBeforeSwitchResult,
|
SessionBeforeSwitchResult,
|
||||||
SessionBeforeTreeResult,
|
SessionBeforeTreeResult,
|
||||||
TreePreparation,
|
TreePreparation,
|
||||||
|
|
@ -1831,32 +1831,32 @@ export class AgentSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a branch from a specific entry.
|
* Create a fork from a specific entry.
|
||||||
* Emits before_branch/branch session events to extensions.
|
* Emits before_fork/fork session events to extensions.
|
||||||
*
|
*
|
||||||
* @param entryId ID of the entry to branch from
|
* @param entryId ID of the entry to fork from
|
||||||
* @returns Object with:
|
* @returns Object with:
|
||||||
* - selectedText: The text of the selected user message (for editor pre-fill)
|
* - selectedText: The text of the selected user message (for editor pre-fill)
|
||||||
* - cancelled: True if an extension cancelled the branch
|
* - cancelled: True if an extension cancelled the fork
|
||||||
*/
|
*/
|
||||||
async branch(entryId: string): Promise<{ selectedText: string; cancelled: boolean }> {
|
async fork(entryId: string): Promise<{ selectedText: string; cancelled: boolean }> {
|
||||||
const previousSessionFile = this.sessionFile;
|
const previousSessionFile = this.sessionFile;
|
||||||
const selectedEntry = this.sessionManager.getEntry(entryId);
|
const selectedEntry = this.sessionManager.getEntry(entryId);
|
||||||
|
|
||||||
if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
|
if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
|
||||||
throw new Error("Invalid entry ID for branching");
|
throw new Error("Invalid entry ID for forking");
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedText = this._extractUserMessageText(selectedEntry.message.content);
|
const selectedText = this._extractUserMessageText(selectedEntry.message.content);
|
||||||
|
|
||||||
let skipConversationRestore = false;
|
let skipConversationRestore = false;
|
||||||
|
|
||||||
// Emit session_before_branch event (can be cancelled)
|
// Emit session_before_fork event (can be cancelled)
|
||||||
if (this._extensionRunner?.hasHandlers("session_before_branch")) {
|
if (this._extensionRunner?.hasHandlers("session_before_fork")) {
|
||||||
const result = (await this._extensionRunner.emit({
|
const result = (await this._extensionRunner.emit({
|
||||||
type: "session_before_branch",
|
type: "session_before_fork",
|
||||||
entryId,
|
entryId,
|
||||||
})) as SessionBeforeBranchResult | undefined;
|
})) as SessionBeforeForkResult | undefined;
|
||||||
|
|
||||||
if (result?.cancel) {
|
if (result?.cancel) {
|
||||||
return { selectedText, cancelled: true };
|
return { selectedText, cancelled: true };
|
||||||
|
|
@ -1877,15 +1877,15 @@ export class AgentSession {
|
||||||
// Reload messages from entries (works for both file and in-memory mode)
|
// Reload messages from entries (works for both file and in-memory mode)
|
||||||
const sessionContext = this.sessionManager.buildSessionContext();
|
const sessionContext = this.sessionManager.buildSessionContext();
|
||||||
|
|
||||||
// Emit session_branch event to extensions (after branch completes)
|
// Emit session_fork event to extensions (after fork completes)
|
||||||
if (this._extensionRunner) {
|
if (this._extensionRunner) {
|
||||||
await this._extensionRunner.emit({
|
await this._extensionRunner.emit({
|
||||||
type: "session_branch",
|
type: "session_fork",
|
||||||
previousSessionFile,
|
previousSessionFile,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit session event to custom tools (with reason "branch")
|
// Emit session event to custom tools (with reason "fork")
|
||||||
|
|
||||||
if (!skipConversationRestore) {
|
if (!skipConversationRestore) {
|
||||||
this.agent.replaceMessages(sessionContext.messages);
|
this.agent.replaceMessages(sessionContext.messages);
|
||||||
|
|
@ -1900,7 +1900,7 @@ export class AgentSession {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to a different node in the session tree.
|
* Navigate to a different node in the session tree.
|
||||||
* Unlike branch() which creates a new session file, this stays in the same file.
|
* Unlike fork() which creates a new session file, this stays in the same file.
|
||||||
*
|
*
|
||||||
* @param targetId The entry ID to navigate to
|
* @param targetId The entry ID to navigate to
|
||||||
* @param options.summarize Whether user wants to summarize abandoned branch
|
* @param options.summarize Whether user wants to summarize abandoned branch
|
||||||
|
|
@ -2061,9 +2061,9 @@ export class AgentSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all user messages from session for branch selector.
|
* Get all user messages from session for fork selector.
|
||||||
*/
|
*/
|
||||||
getUserMessagesForBranching(): Array<{ entryId: string; text: string }> {
|
getUserMessagesForForking(): Array<{ entryId: string; text: string }> {
|
||||||
const entries = this.sessionManager.getEntries();
|
const entries = this.sessionManager.getEntries();
|
||||||
const result: Array<{ entryId: string; text: string }> = [];
|
const result: Array<{ entryId: string; text: string }> = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ export {
|
||||||
loadExtensions,
|
loadExtensions,
|
||||||
} from "./loader.js";
|
} from "./loader.js";
|
||||||
export type {
|
export type {
|
||||||
BranchHandler,
|
|
||||||
ExtensionErrorListener,
|
ExtensionErrorListener,
|
||||||
|
ForkHandler,
|
||||||
NavigateTreeHandler,
|
NavigateTreeHandler,
|
||||||
NewSessionHandler,
|
NewSessionHandler,
|
||||||
ShutdownHandler,
|
ShutdownHandler,
|
||||||
|
|
@ -75,17 +75,17 @@ export type {
|
||||||
RegisteredTool,
|
RegisteredTool,
|
||||||
SendMessageHandler,
|
SendMessageHandler,
|
||||||
SendUserMessageHandler,
|
SendUserMessageHandler,
|
||||||
SessionBeforeBranchEvent,
|
|
||||||
SessionBeforeBranchResult,
|
|
||||||
SessionBeforeCompactEvent,
|
SessionBeforeCompactEvent,
|
||||||
SessionBeforeCompactResult,
|
SessionBeforeCompactResult,
|
||||||
|
SessionBeforeForkEvent,
|
||||||
|
SessionBeforeForkResult,
|
||||||
SessionBeforeSwitchEvent,
|
SessionBeforeSwitchEvent,
|
||||||
SessionBeforeSwitchResult,
|
SessionBeforeSwitchResult,
|
||||||
SessionBeforeTreeEvent,
|
SessionBeforeTreeEvent,
|
||||||
SessionBeforeTreeResult,
|
SessionBeforeTreeResult,
|
||||||
SessionBranchEvent,
|
|
||||||
SessionCompactEvent,
|
SessionCompactEvent,
|
||||||
SessionEvent,
|
SessionEvent,
|
||||||
|
SessionForkEvent,
|
||||||
SessionShutdownEvent,
|
SessionShutdownEvent,
|
||||||
// Events - Session
|
// Events - Session
|
||||||
SessionStartEvent,
|
SessionStartEvent,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export type NewSessionHandler = (options?: {
|
||||||
setup?: (sessionManager: SessionManager) => Promise<void>;
|
setup?: (sessionManager: SessionManager) => Promise<void>;
|
||||||
}) => Promise<{ cancelled: boolean }>;
|
}) => Promise<{ cancelled: boolean }>;
|
||||||
|
|
||||||
export type BranchHandler = (entryId: string) => Promise<{ cancelled: boolean }>;
|
export type ForkHandler = (entryId: string) => Promise<{ cancelled: boolean }>;
|
||||||
|
|
||||||
export type NavigateTreeHandler = (
|
export type NavigateTreeHandler = (
|
||||||
targetId: string,
|
targetId: string,
|
||||||
|
|
@ -111,7 +111,7 @@ export class ExtensionRunner {
|
||||||
private abortFn: () => void = () => {};
|
private abortFn: () => void = () => {};
|
||||||
private hasPendingMessagesFn: () => boolean = () => false;
|
private hasPendingMessagesFn: () => boolean = () => false;
|
||||||
private newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
|
private newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
|
||||||
private branchHandler: BranchHandler = async () => ({ cancelled: false });
|
private forkHandler: ForkHandler = async () => ({ cancelled: false });
|
||||||
private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
|
private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
|
||||||
private shutdownHandler: ShutdownHandler = () => {};
|
private shutdownHandler: ShutdownHandler = () => {};
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ export class ExtensionRunner {
|
||||||
if (commandContextActions) {
|
if (commandContextActions) {
|
||||||
this.waitForIdleFn = commandContextActions.waitForIdle;
|
this.waitForIdleFn = commandContextActions.waitForIdle;
|
||||||
this.newSessionHandler = commandContextActions.newSession;
|
this.newSessionHandler = commandContextActions.newSession;
|
||||||
this.branchHandler = commandContextActions.branch;
|
this.forkHandler = commandContextActions.fork;
|
||||||
this.navigateTreeHandler = commandContextActions.navigateTree;
|
this.navigateTreeHandler = commandContextActions.navigateTree;
|
||||||
}
|
}
|
||||||
this.uiContext = uiContext ?? noOpUIContext;
|
this.uiContext = uiContext ?? noOpUIContext;
|
||||||
|
|
@ -329,17 +329,17 @@ export class ExtensionRunner {
|
||||||
...this.createContext(),
|
...this.createContext(),
|
||||||
waitForIdle: () => this.waitForIdleFn(),
|
waitForIdle: () => this.waitForIdleFn(),
|
||||||
newSession: (options) => this.newSessionHandler(options),
|
newSession: (options) => this.newSessionHandler(options),
|
||||||
branch: (entryId) => this.branchHandler(entryId),
|
fork: (entryId) => this.forkHandler(entryId),
|
||||||
navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),
|
navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private isSessionBeforeEvent(
|
private isSessionBeforeEvent(
|
||||||
type: string,
|
type: string,
|
||||||
): type is "session_before_switch" | "session_before_branch" | "session_before_compact" | "session_before_tree" {
|
): type is "session_before_switch" | "session_before_fork" | "session_before_compact" | "session_before_tree" {
|
||||||
return (
|
return (
|
||||||
type === "session_before_switch" ||
|
type === "session_before_switch" ||
|
||||||
type === "session_before_branch" ||
|
type === "session_before_fork" ||
|
||||||
type === "session_before_compact" ||
|
type === "session_before_compact" ||
|
||||||
type === "session_before_tree"
|
type === "session_before_tree"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -218,8 +218,8 @@ export interface ExtensionCommandContext extends ExtensionContext {
|
||||||
setup?: (sessionManager: SessionManager) => Promise<void>;
|
setup?: (sessionManager: SessionManager) => Promise<void>;
|
||||||
}): Promise<{ cancelled: boolean }>;
|
}): Promise<{ cancelled: boolean }>;
|
||||||
|
|
||||||
/** Branch from a specific entry, creating a new session file. */
|
/** Fork from a specific entry, creating a new session file. */
|
||||||
branch(entryId: string): Promise<{ cancelled: boolean }>;
|
fork(entryId: string): Promise<{ cancelled: boolean }>;
|
||||||
|
|
||||||
/** Navigate to a different point in the session tree. */
|
/** Navigate to a different point in the session tree. */
|
||||||
navigateTree(targetId: string, options?: { summarize?: boolean }): Promise<{ cancelled: boolean }>;
|
navigateTree(targetId: string, options?: { summarize?: boolean }): Promise<{ cancelled: boolean }>;
|
||||||
|
|
@ -289,15 +289,15 @@ export interface SessionSwitchEvent {
|
||||||
previousSessionFile: string | undefined;
|
previousSessionFile: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fired before branching a session (can be cancelled) */
|
/** Fired before forking a session (can be cancelled) */
|
||||||
export interface SessionBeforeBranchEvent {
|
export interface SessionBeforeForkEvent {
|
||||||
type: "session_before_branch";
|
type: "session_before_fork";
|
||||||
entryId: string;
|
entryId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fired after branching a session */
|
/** Fired after forking a session */
|
||||||
export interface SessionBranchEvent {
|
export interface SessionForkEvent {
|
||||||
type: "session_branch";
|
type: "session_fork";
|
||||||
previousSessionFile: string | undefined;
|
previousSessionFile: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,8 +351,8 @@ export type SessionEvent =
|
||||||
| SessionStartEvent
|
| SessionStartEvent
|
||||||
| SessionBeforeSwitchEvent
|
| SessionBeforeSwitchEvent
|
||||||
| SessionSwitchEvent
|
| SessionSwitchEvent
|
||||||
| SessionBeforeBranchEvent
|
| SessionBeforeForkEvent
|
||||||
| SessionBranchEvent
|
| SessionForkEvent
|
||||||
| SessionBeforeCompactEvent
|
| SessionBeforeCompactEvent
|
||||||
| SessionCompactEvent
|
| SessionCompactEvent
|
||||||
| SessionShutdownEvent
|
| SessionShutdownEvent
|
||||||
|
|
@ -577,7 +577,7 @@ export interface SessionBeforeSwitchResult {
|
||||||
cancel?: boolean;
|
cancel?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionBeforeBranchResult {
|
export interface SessionBeforeForkResult {
|
||||||
cancel?: boolean;
|
cancel?: boolean;
|
||||||
skipConversationRestore?: boolean;
|
skipConversationRestore?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -641,11 +641,8 @@ export interface ExtensionAPI {
|
||||||
handler: ExtensionHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>,
|
handler: ExtensionHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>,
|
||||||
): void;
|
): void;
|
||||||
on(event: "session_switch", handler: ExtensionHandler<SessionSwitchEvent>): void;
|
on(event: "session_switch", handler: ExtensionHandler<SessionSwitchEvent>): void;
|
||||||
on(
|
on(event: "session_before_fork", handler: ExtensionHandler<SessionBeforeForkEvent, SessionBeforeForkResult>): void;
|
||||||
event: "session_before_branch",
|
on(event: "session_fork", handler: ExtensionHandler<SessionForkEvent>): void;
|
||||||
handler: ExtensionHandler<SessionBeforeBranchEvent, SessionBeforeBranchResult>,
|
|
||||||
): void;
|
|
||||||
on(event: "session_branch", handler: ExtensionHandler<SessionBranchEvent>): void;
|
|
||||||
on(
|
on(
|
||||||
event: "session_before_compact",
|
event: "session_before_compact",
|
||||||
handler: ExtensionHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
|
handler: ExtensionHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
|
||||||
|
|
@ -858,7 +855,7 @@ export interface ExtensionCommandContextActions {
|
||||||
parentSession?: string;
|
parentSession?: string;
|
||||||
setup?: (sessionManager: SessionManager) => Promise<void>;
|
setup?: (sessionManager: SessionManager) => Promise<void>;
|
||||||
}) => Promise<{ cancelled: boolean }>;
|
}) => Promise<{ cancelled: boolean }>;
|
||||||
branch: (entryId: string) => Promise<{ cancelled: boolean }>;
|
fork: (entryId: string) => Promise<{ cancelled: boolean }>;
|
||||||
navigateTree: (targetId: string, options?: { summarize?: boolean }) => Promise<{ cancelled: boolean }>;
|
navigateTree: (targetId: string, options?: { summarize?: boolean }) => Promise<{ cancelled: boolean }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,12 @@ export {
|
||||||
type LoadExtensionsResult,
|
type LoadExtensionsResult,
|
||||||
type MessageRenderer,
|
type MessageRenderer,
|
||||||
type RegisteredCommand,
|
type RegisteredCommand,
|
||||||
type SessionBeforeBranchEvent,
|
|
||||||
type SessionBeforeCompactEvent,
|
type SessionBeforeCompactEvent,
|
||||||
|
type SessionBeforeForkEvent,
|
||||||
type SessionBeforeSwitchEvent,
|
type SessionBeforeSwitchEvent,
|
||||||
type SessionBeforeTreeEvent,
|
type SessionBeforeTreeEvent,
|
||||||
type SessionBranchEvent,
|
|
||||||
type SessionCompactEvent,
|
type SessionCompactEvent,
|
||||||
|
type SessionForkEvent,
|
||||||
type SessionShutdownEvent,
|
type SessionShutdownEvent,
|
||||||
type SessionStartEvent,
|
type SessionStartEvent,
|
||||||
type SessionSwitchEvent,
|
type SessionSwitchEvent,
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export interface Settings {
|
||||||
terminal?: TerminalSettings;
|
terminal?: TerminalSettings;
|
||||||
images?: ImageSettings;
|
images?: ImageSettings;
|
||||||
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
|
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
|
||||||
doubleEscapeAction?: "branch" | "tree"; // Action for double-escape with empty editor (default: "tree")
|
doubleEscapeAction?: "fork" | "tree"; // Action for double-escape with empty editor (default: "tree")
|
||||||
thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels
|
thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -452,11 +452,11 @@ export class SettingsManager {
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
getDoubleEscapeAction(): "branch" | "tree" {
|
getDoubleEscapeAction(): "fork" | "tree" {
|
||||||
return this.settings.doubleEscapeAction ?? "tree";
|
return this.settings.doubleEscapeAction ?? "tree";
|
||||||
}
|
}
|
||||||
|
|
||||||
setDoubleEscapeAction(action: "branch" | "tree"): void {
|
setDoubleEscapeAction(action: "fork" | "tree"): void {
|
||||||
this.globalSettings.doubleEscapeAction = action;
|
this.globalSettings.doubleEscapeAction = action;
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,12 +67,12 @@ export type {
|
||||||
MessageRenderOptions,
|
MessageRenderOptions,
|
||||||
RegisteredCommand,
|
RegisteredCommand,
|
||||||
RegisteredTool,
|
RegisteredTool,
|
||||||
SessionBeforeBranchEvent,
|
|
||||||
SessionBeforeCompactEvent,
|
SessionBeforeCompactEvent,
|
||||||
|
SessionBeforeForkEvent,
|
||||||
SessionBeforeSwitchEvent,
|
SessionBeforeSwitchEvent,
|
||||||
SessionBeforeTreeEvent,
|
SessionBeforeTreeEvent,
|
||||||
SessionBranchEvent,
|
|
||||||
SessionCompactEvent,
|
SessionCompactEvent,
|
||||||
|
SessionForkEvent,
|
||||||
SessionShutdownEvent,
|
SessionShutdownEvent,
|
||||||
SessionStartEvent,
|
SessionStartEvent,
|
||||||
SessionSwitchEvent,
|
SessionSwitchEvent,
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export interface SettingsConfig {
|
||||||
availableThemes: string[];
|
availableThemes: string[];
|
||||||
hideThinkingBlock: boolean;
|
hideThinkingBlock: boolean;
|
||||||
collapseChangelog: boolean;
|
collapseChangelog: boolean;
|
||||||
doubleEscapeAction: "branch" | "tree";
|
doubleEscapeAction: "fork" | "tree";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsCallbacks {
|
export interface SettingsCallbacks {
|
||||||
|
|
@ -51,7 +51,7 @@ export interface SettingsCallbacks {
|
||||||
onThemePreview?: (theme: string) => void;
|
onThemePreview?: (theme: string) => void;
|
||||||
onHideThinkingBlockChange: (hidden: boolean) => void;
|
onHideThinkingBlockChange: (hidden: boolean) => void;
|
||||||
onCollapseChangelogChange: (collapsed: boolean) => void;
|
onCollapseChangelogChange: (collapsed: boolean) => void;
|
||||||
onDoubleEscapeActionChange: (action: "branch" | "tree") => void;
|
onDoubleEscapeActionChange: (action: "fork" | "tree") => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,7 +171,7 @@ export class SettingsSelectorComponent extends Container {
|
||||||
label: "Double-escape action",
|
label: "Double-escape action",
|
||||||
description: "Action when pressing Escape twice with empty editor",
|
description: "Action when pressing Escape twice with empty editor",
|
||||||
currentValue: config.doubleEscapeAction,
|
currentValue: config.doubleEscapeAction,
|
||||||
values: ["tree", "branch"],
|
values: ["tree", "fork"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "thinking",
|
id: "thinking",
|
||||||
|
|
@ -304,7 +304,7 @@ export class SettingsSelectorComponent extends Container {
|
||||||
callbacks.onCollapseChangelogChange(newValue === "true");
|
callbacks.onCollapseChangelogChange(newValue === "true");
|
||||||
break;
|
break;
|
||||||
case "double-escape-action":
|
case "double-escape-action":
|
||||||
callbacks.onDoubleEscapeActionChange(newValue as "branch" | "tree");
|
callbacks.onDoubleEscapeActionChange(newValue as "fork" | "tree");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ export class InteractiveMode {
|
||||||
{ name: "session", description: "Show session info and stats" },
|
{ name: "session", description: "Show session info and stats" },
|
||||||
{ name: "changelog", description: "Show changelog entries" },
|
{ name: "changelog", description: "Show changelog entries" },
|
||||||
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
||||||
{ name: "branch", description: "Create a new branch from a previous message" },
|
{ name: "fork", description: "Create a new fork from a previous message" },
|
||||||
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
||||||
{ name: "login", description: "Login with OAuth provider" },
|
{ name: "login", description: "Login with OAuth provider" },
|
||||||
{ name: "logout", description: "Logout from OAuth provider" },
|
{ name: "logout", description: "Logout from OAuth provider" },
|
||||||
|
|
@ -730,8 +730,8 @@ export class InteractiveMode {
|
||||||
|
|
||||||
return { cancelled: false };
|
return { cancelled: false };
|
||||||
},
|
},
|
||||||
branch: async (entryId) => {
|
fork: async (entryId) => {
|
||||||
const result = await this.session.branch(entryId);
|
const result = await this.session.fork(entryId);
|
||||||
if (result.cancelled) {
|
if (result.cancelled) {
|
||||||
return { cancelled: true };
|
return { cancelled: true };
|
||||||
}
|
}
|
||||||
|
|
@ -739,7 +739,7 @@ export class InteractiveMode {
|
||||||
this.chatContainer.clear();
|
this.chatContainer.clear();
|
||||||
this.renderInitialMessages();
|
this.renderInitialMessages();
|
||||||
this.editor.setText(result.selectedText);
|
this.editor.setText(result.selectedText);
|
||||||
this.showStatus("Branched to new session");
|
this.showStatus("Forked to new session");
|
||||||
|
|
||||||
return { cancelled: false };
|
return { cancelled: false };
|
||||||
},
|
},
|
||||||
|
|
@ -1336,7 +1336,7 @@ export class InteractiveMode {
|
||||||
this.isBashMode = false;
|
this.isBashMode = false;
|
||||||
this.updateEditorBorderColor();
|
this.updateEditorBorderColor();
|
||||||
} else if (!this.editor.getText().trim()) {
|
} else if (!this.editor.getText().trim()) {
|
||||||
// Double-escape with empty editor triggers /tree or /branch based on setting
|
// Double-escape with empty editor triggers /tree or /fork based on setting
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - this.lastEscapeTime < 500) {
|
if (now - this.lastEscapeTime < 500) {
|
||||||
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
||||||
|
|
@ -1456,7 +1456,7 @@ export class InteractiveMode {
|
||||||
this.editor.setText("");
|
this.editor.setText("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text === "/branch") {
|
if (text === "/fork") {
|
||||||
this.showUserMessageSelector();
|
this.showUserMessageSelector();
|
||||||
this.editor.setText("");
|
this.editor.setText("");
|
||||||
return;
|
return;
|
||||||
|
|
@ -2738,10 +2738,10 @@ export class InteractiveMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
private showUserMessageSelector(): void {
|
private showUserMessageSelector(): void {
|
||||||
const userMessages = this.session.getUserMessagesForBranching();
|
const userMessages = this.session.getUserMessagesForForking();
|
||||||
|
|
||||||
if (userMessages.length === 0) {
|
if (userMessages.length === 0) {
|
||||||
this.showStatus("No messages to branch from");
|
this.showStatus("No messages to fork from");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2749,9 +2749,9 @@ export class InteractiveMode {
|
||||||
const selector = new UserMessageSelectorComponent(
|
const selector = new UserMessageSelectorComponent(
|
||||||
userMessages.map((m) => ({ id: m.entryId, text: m.text })),
|
userMessages.map((m) => ({ id: m.entryId, text: m.text })),
|
||||||
async (entryId) => {
|
async (entryId) => {
|
||||||
const result = await this.session.branch(entryId);
|
const result = await this.session.fork(entryId);
|
||||||
if (result.cancelled) {
|
if (result.cancelled) {
|
||||||
// Extension cancelled the branch
|
// Extension cancelled the fork
|
||||||
done();
|
done();
|
||||||
this.ui.requestRender();
|
this.ui.requestRender();
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,8 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
||||||
}
|
}
|
||||||
return { cancelled: !success };
|
return { cancelled: !success };
|
||||||
},
|
},
|
||||||
branch: async (entryId) => {
|
fork: async (entryId) => {
|
||||||
const result = await session.branch(entryId);
|
const result = await session.fork(entryId);
|
||||||
return { cancelled: result.cancelled };
|
return { cancelled: result.cancelled };
|
||||||
},
|
},
|
||||||
navigateTree: async (targetId, options) => {
|
navigateTree: async (targetId, options) => {
|
||||||
|
|
|
||||||
|
|
@ -338,19 +338,19 @@ export class RpcClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Branch from a specific message.
|
* Fork from a specific message.
|
||||||
* @returns Object with `text` (the message text) and `cancelled` (if extension cancelled)
|
* @returns Object with `text` (the message text) and `cancelled` (if extension cancelled)
|
||||||
*/
|
*/
|
||||||
async branch(entryId: string): Promise<{ text: string; cancelled: boolean }> {
|
async fork(entryId: string): Promise<{ text: string; cancelled: boolean }> {
|
||||||
const response = await this.send({ type: "branch", entryId });
|
const response = await this.send({ type: "fork", entryId });
|
||||||
return this.getData(response);
|
return this.getData(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get messages available for branching.
|
* Get messages available for forking.
|
||||||
*/
|
*/
|
||||||
async getBranchMessages(): Promise<Array<{ entryId: string; text: string }>> {
|
async getForkMessages(): Promise<Array<{ entryId: string; text: string }>> {
|
||||||
const response = await this.send({ type: "get_branch_messages" });
|
const response = await this.send({ type: "get_fork_messages" });
|
||||||
return this.getData<{ messages: Array<{ entryId: string; text: string }> }>(response).messages;
|
return this.getData<{ messages: Array<{ entryId: string; text: string }> }>(response).messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -300,8 +300,8 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
||||||
}
|
}
|
||||||
return { cancelled: !success };
|
return { cancelled: !success };
|
||||||
},
|
},
|
||||||
branch: async (entryId) => {
|
fork: async (entryId) => {
|
||||||
const result = await session.branch(entryId);
|
const result = await session.fork(entryId);
|
||||||
return { cancelled: result.cancelled };
|
return { cancelled: result.cancelled };
|
||||||
},
|
},
|
||||||
navigateTree: async (targetId, options) => {
|
navigateTree: async (targetId, options) => {
|
||||||
|
|
@ -508,14 +508,14 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
||||||
return success(id, "switch_session", { cancelled });
|
return success(id, "switch_session", { cancelled });
|
||||||
}
|
}
|
||||||
|
|
||||||
case "branch": {
|
case "fork": {
|
||||||
const result = await session.branch(command.entryId);
|
const result = await session.fork(command.entryId);
|
||||||
return success(id, "branch", { text: result.selectedText, cancelled: result.cancelled });
|
return success(id, "fork", { text: result.selectedText, cancelled: result.cancelled });
|
||||||
}
|
}
|
||||||
|
|
||||||
case "get_branch_messages": {
|
case "get_fork_messages": {
|
||||||
const messages = session.getUserMessagesForBranching();
|
const messages = session.getUserMessagesForForking();
|
||||||
return success(id, "get_branch_messages", { messages });
|
return success(id, "get_fork_messages", { messages });
|
||||||
}
|
}
|
||||||
|
|
||||||
case "get_last_assistant_text": {
|
case "get_last_assistant_text": {
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ export type RpcCommand =
|
||||||
| { id?: string; type: "get_session_stats" }
|
| { id?: string; type: "get_session_stats" }
|
||||||
| { id?: string; type: "export_html"; outputPath?: string }
|
| { id?: string; type: "export_html"; outputPath?: string }
|
||||||
| { id?: string; type: "switch_session"; sessionPath: string }
|
| { id?: string; type: "switch_session"; sessionPath: string }
|
||||||
| { id?: string; type: "branch"; entryId: string }
|
| { id?: string; type: "fork"; entryId: string }
|
||||||
| { id?: string; type: "get_branch_messages" }
|
| { id?: string; type: "get_fork_messages" }
|
||||||
| { id?: string; type: "get_last_assistant_text" }
|
| { id?: string; type: "get_last_assistant_text" }
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
|
|
@ -149,11 +149,11 @@ export type RpcResponse =
|
||||||
| { id?: string; type: "response"; command: "get_session_stats"; success: true; data: SessionStats }
|
| { id?: string; type: "response"; command: "get_session_stats"; success: true; data: SessionStats }
|
||||||
| { id?: string; type: "response"; command: "export_html"; success: true; data: { path: string } }
|
| { id?: string; type: "response"; command: "export_html"; success: true; data: { path: string } }
|
||||||
| { id?: string; type: "response"; command: "switch_session"; success: true; data: { cancelled: boolean } }
|
| { id?: string; type: "response"; command: "switch_session"; success: true; data: { cancelled: boolean } }
|
||||||
| { id?: string; type: "response"; command: "branch"; success: true; data: { text: string; cancelled: boolean } }
|
| { id?: string; type: "response"; command: "fork"; success: true; data: { text: string; cancelled: boolean } }
|
||||||
| {
|
| {
|
||||||
id?: string;
|
id?: string;
|
||||||
type: "response";
|
type: "response";
|
||||||
command: "get_branch_messages";
|
command: "get_fork_messages";
|
||||||
success: true;
|
success: true;
|
||||||
data: { messages: Array<{ entryId: string; text: string }> };
|
data: { messages: Array<{ entryId: string; text: string }> };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* Tests for AgentSession branching behavior.
|
* Tests for AgentSession forking behavior.
|
||||||
*
|
*
|
||||||
* These tests verify:
|
* These tests verify:
|
||||||
* - Branching from a single message works
|
* - Forking from a single message works
|
||||||
* - Branching in --no-session mode (in-memory only)
|
* - Forking in --no-session mode (in-memory only)
|
||||||
* - getUserMessagesForBranching returns correct entries
|
* - getUserMessagesForForking returns correct entries
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
||||||
|
|
@ -21,7 +21,7 @@ import { SettingsManager } from "../src/core/settings-manager.js";
|
||||||
import { codingTools } from "../src/core/tools/index.js";
|
import { codingTools } from "../src/core/tools/index.js";
|
||||||
import { API_KEY } from "./utilities.js";
|
import { API_KEY } from "./utilities.js";
|
||||||
|
|
||||||
describe.skipIf(!API_KEY)("AgentSession branching", () => {
|
describe.skipIf(!API_KEY)("AgentSession forking", () => {
|
||||||
let session: AgentSession;
|
let session: AgentSession;
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
let sessionManager: SessionManager;
|
let sessionManager: SessionManager;
|
||||||
|
|
@ -70,32 +70,32 @@ describe.skipIf(!API_KEY)("AgentSession branching", () => {
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should allow branching from single message", async () => {
|
it("should allow forking from single message", async () => {
|
||||||
createSession();
|
createSession();
|
||||||
|
|
||||||
// Send one message
|
// Send one message
|
||||||
await session.prompt("Say hello");
|
await session.prompt("Say hello");
|
||||||
await session.agent.waitForIdle();
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
// Should have exactly 1 user message available for branching
|
// Should have exactly 1 user message available for forking
|
||||||
const userMessages = session.getUserMessagesForBranching();
|
const userMessages = session.getUserMessagesForForking();
|
||||||
expect(userMessages.length).toBe(1);
|
expect(userMessages.length).toBe(1);
|
||||||
expect(userMessages[0].text).toBe("Say hello");
|
expect(userMessages[0].text).toBe("Say hello");
|
||||||
|
|
||||||
// Branch from the first message
|
// Fork from the first message
|
||||||
const result = await session.branch(userMessages[0].entryId);
|
const result = await session.fork(userMessages[0].entryId);
|
||||||
expect(result.selectedText).toBe("Say hello");
|
expect(result.selectedText).toBe("Say hello");
|
||||||
expect(result.cancelled).toBe(false);
|
expect(result.cancelled).toBe(false);
|
||||||
|
|
||||||
// After branching, conversation should be empty (branched before the first message)
|
// After forking, conversation should be empty (forked before the first message)
|
||||||
expect(session.messages.length).toBe(0);
|
expect(session.messages.length).toBe(0);
|
||||||
|
|
||||||
// Session file should exist (new branch)
|
// Session file should exist (new fork)
|
||||||
expect(session.sessionFile).not.toBeNull();
|
expect(session.sessionFile).not.toBeNull();
|
||||||
expect(existsSync(session.sessionFile!)).toBe(true);
|
expect(existsSync(session.sessionFile!)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support in-memory branching in --no-session mode", async () => {
|
it("should support in-memory forking in --no-session mode", async () => {
|
||||||
createSession(true);
|
createSession(true);
|
||||||
|
|
||||||
// Verify sessions are disabled
|
// Verify sessions are disabled
|
||||||
|
|
@ -106,25 +106,25 @@ describe.skipIf(!API_KEY)("AgentSession branching", () => {
|
||||||
await session.agent.waitForIdle();
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
// Should have 1 user message
|
// Should have 1 user message
|
||||||
const userMessages = session.getUserMessagesForBranching();
|
const userMessages = session.getUserMessagesForForking();
|
||||||
expect(userMessages.length).toBe(1);
|
expect(userMessages.length).toBe(1);
|
||||||
|
|
||||||
// Verify we have messages before branching
|
// Verify we have messages before forking
|
||||||
expect(session.messages.length).toBeGreaterThan(0);
|
expect(session.messages.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Branch from the first message
|
// Fork from the first message
|
||||||
const result = await session.branch(userMessages[0].entryId);
|
const result = await session.fork(userMessages[0].entryId);
|
||||||
expect(result.selectedText).toBe("Say hi");
|
expect(result.selectedText).toBe("Say hi");
|
||||||
expect(result.cancelled).toBe(false);
|
expect(result.cancelled).toBe(false);
|
||||||
|
|
||||||
// After branching, conversation should be empty
|
// After forking, conversation should be empty
|
||||||
expect(session.messages.length).toBe(0);
|
expect(session.messages.length).toBe(0);
|
||||||
|
|
||||||
// Session file should still be undefined (no file created)
|
// Session file should still be undefined (no file created)
|
||||||
expect(session.sessionFile).toBeUndefined();
|
expect(session.sessionFile).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should branch from middle of conversation", async () => {
|
it("should fork from middle of conversation", async () => {
|
||||||
createSession();
|
createSession();
|
||||||
|
|
||||||
// Send multiple messages
|
// Send multiple messages
|
||||||
|
|
@ -138,15 +138,15 @@ describe.skipIf(!API_KEY)("AgentSession branching", () => {
|
||||||
await session.agent.waitForIdle();
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
// Should have 3 user messages
|
// Should have 3 user messages
|
||||||
const userMessages = session.getUserMessagesForBranching();
|
const userMessages = session.getUserMessagesForForking();
|
||||||
expect(userMessages.length).toBe(3);
|
expect(userMessages.length).toBe(3);
|
||||||
|
|
||||||
// Branch from second message (keeps first message + response)
|
// Fork from second message (keeps first message + response)
|
||||||
const secondMessage = userMessages[1];
|
const secondMessage = userMessages[1];
|
||||||
const result = await session.branch(secondMessage.entryId);
|
const result = await session.fork(secondMessage.entryId);
|
||||||
expect(result.selectedText).toBe("Say two");
|
expect(result.selectedText).toBe("Say two");
|
||||||
|
|
||||||
// After branching, should have first user message + assistant response
|
// After forking, should have first user message + assistant response
|
||||||
expect(session.messages.length).toBe(2);
|
expect(session.messages.length).toBe(2);
|
||||||
expect(session.messages[0].role).toBe("user");
|
expect(session.messages[0].role).toBe("user");
|
||||||
expect(session.messages[1].role).toBe("assistant");
|
expect(session.messages[1].role).toBe("assistant");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue