From df3f5f41c0b93245e589520bdd10cea02ad93fab Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 11 Jan 2026 23:12:18 +0100 Subject: [PATCH] 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 --- packages/coding-agent/CHANGELOG.md | 9 ++++ packages/coding-agent/README.md | 10 ++-- packages/coding-agent/docs/extensions.md | 20 ++++---- packages/coding-agent/docs/rpc.md | 20 ++++---- packages/coding-agent/docs/sdk.md | 4 +- packages/coding-agent/docs/session.md | 2 +- packages/coding-agent/docs/tree.md | 8 ++-- .../examples/extensions/README.md | 6 +-- .../extensions/confirm-destructive.ts | 10 ++-- .../examples/extensions/dirty-repo-guard.ts | 4 +- .../examples/extensions/git-checkpoint.ts | 6 +-- .../coding-agent/examples/extensions/todo.ts | 2 +- .../coding-agent/examples/extensions/tools.ts | 4 +- .../coding-agent/src/core/agent-session.ts | 34 +++++++------- .../coding-agent/src/core/extensions/index.ts | 8 ++-- .../src/core/extensions/runner.ts | 12 ++--- .../coding-agent/src/core/extensions/types.ts | 31 ++++++------- packages/coding-agent/src/core/index.ts | 4 +- .../coding-agent/src/core/settings-manager.ts | 6 +-- packages/coding-agent/src/index.ts | 4 +- .../components/settings-selector.ts | 8 ++-- .../src/modes/interactive/interactive-mode.ts | 20 ++++---- packages/coding-agent/src/modes/print-mode.ts | 4 +- .../coding-agent/src/modes/rpc/rpc-client.ts | 12 ++--- .../coding-agent/src/modes/rpc/rpc-mode.ts | 16 +++---- .../coding-agent/src/modes/rpc/rpc-types.ts | 8 ++-- .../test/agent-session-branching.test.ts | 46 +++++++++---------- 27 files changed, 162 insertions(+), 156 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b612023c..4f60bcde 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,15 @@ ## [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 - `/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)) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index dbc0df90..2ad84712 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -243,7 +243,7 @@ The agent reads, writes, and edits files, and executes commands via bash. | `/hotkeys` | Show all keyboard shortcuts | | `/changelog` | Display full version history | | `/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) | | `/login` | OAuth login for subscription-based models | | `/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 - 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 -2. Select a message to branch from +2. Select a message to fork from 3. Creates new session with history up to that point 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 commands** - Add `/commands` for users (e.g., `/deploy`, `/stats`) - **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 - **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_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.registerCommand("increment", { diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 7db3085d..809fed6c 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -278,9 +278,9 @@ user sends another prompt ◄───────────────── ├─► session_before_switch (can cancel) └─► session_switch -/branch - ├─► session_before_branch (can cancel) - └─► session_branch +/fork + ├─► session_before_fork (can cancel) + └─► session_fork /compact or auto-compaction ├─► 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) -#### session_before_branch / session_branch +#### session_before_fork / session_fork -Fired when branching via `/branch`. +Fired when forking via `/fork`. ```typescript -pi.on("session_before_branch", async (event, ctx) => { - // event.entryId - ID of the entry being branched from - return { cancel: true }; // Cancel branch +pi.on("session_before_fork", async (event, ctx) => { + // event.entryId - ID of the entry being forked from + return { cancel: true }; // Cancel fork // 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 }); ``` diff --git a/packages/coding-agent/docs/rpc.md b/packages/coding-agent/docs/rpc.md index ea2ae1ff..9fd0809d 100644 --- a/packages/coding-agent/docs/rpc.md +++ b/packages/coding-agent/docs/rpc.md @@ -541,47 +541,47 @@ If a hook cancelled the switch: {"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 -{"type": "branch", "entryId": "abc123"} +{"type": "fork", "entryId": "abc123"} ``` Response: ```json { "type": "response", - "command": "branch", + "command": "fork", "success": true, "data": {"text": "The original prompt text...", "cancelled": false} } ``` -If a hook cancelled the branch: +If a hook cancelled the fork: ```json { "type": "response", - "command": "branch", + "command": "fork", "success": 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 -{"type": "get_branch_messages"} +{"type": "get_fork_messages"} ``` Response: ```json { "type": "response", - "command": "get_branch_messages", + "command": "get_fork_messages", "success": true, "data": { "messages": [ diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index 1ee24d45..ae974dad 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -108,8 +108,8 @@ interface AgentSession { newSession(options?: { parentSession?: string }): Promise; // Returns false if cancelled by hook switchSession(sessionPath: string): Promise; - // Branching - branch(entryId: string): Promise<{ selectedText: string; cancelled: boolean }>; // Creates new session file + // Forking + 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 // Hook message injection diff --git a/packages/coding-agent/docs/session.md b/packages/coding-agent/docs/session.md index 02b7426e..06aa220c 100644 --- a/packages/coding-agent/docs/session.md +++ b/packages/coding-agent/docs/session.md @@ -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"} ``` -For sessions with a parent (created via `/branch` or `newSession({ parentSession })`): +For sessions with a parent (created via `/fork` or `newSession({ parentSession })`): ```json {"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"} diff --git a/packages/coding-agent/docs/tree.md b/packages/coding-agent/docs/tree.md index 768a1cc0..73e58c99 100644 --- a/packages/coding-agent/docs/tree.md +++ b/packages/coding-agent/docs/tree.md @@ -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. -### Comparison with `/branch` +### Comparison with `/fork` -| Feature | `/branch` | `/tree` | -|---------|-----------|---------| +| 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_branch` / `session_branch` | `session_before_tree` / `session_tree` | +| Events | `session_before_fork` / `session_fork` | `session_before_tree` / `session_tree` | ## Tree UI diff --git a/packages/coding-agent/examples/extensions/README.md b/packages/coding-agent/examples/extensions/README.md index 9f33f011..b7da25bd 100644 --- a/packages/coding-agent/examples/extensions/README.md +++ b/packages/coding-agent/examples/extensions/README.md @@ -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.) | | `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 | ### Custom Tools @@ -53,7 +53,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/ | 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 | ### System Prompt & Compaction @@ -129,7 +129,7 @@ action: Type.Union([Type.Literal("list"), Type.Literal("add")]) **State persistence via details:** ```typescript -// Store state in tool result details for proper branching support +// Store state in tool result details for proper forking support return { content: [{ type: "text", text: "Done" }], details: { todos: [...todos], nextId }, // Persisted in session diff --git a/packages/coding-agent/examples/extensions/confirm-destructive.ts b/packages/coding-agent/examples/extensions/confirm-destructive.ts index 66157113..7a32df82 100644 --- a/packages/coding-agent/examples/extensions/confirm-destructive.ts +++ b/packages/coding-agent/examples/extensions/confirm-destructive.ts @@ -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; - const choice = await ctx.ui.select(`Branch from entry ${event.entryId.slice(0, 8)}?`, [ - "Yes, create branch", + const choice = await ctx.ui.select(`Fork from entry ${event.entryId.slice(0, 8)}?`, [ + "Yes, create fork", "No, stay in current session", ]); - if (choice !== "Yes, create branch") { - ctx.ui.notify("Branch cancelled", "info"); + if (choice !== "Yes, create fork") { + ctx.ui.notify("Fork cancelled", "info"); return { cancel: true }; } }); diff --git a/packages/coding-agent/examples/extensions/dirty-repo-guard.ts b/packages/coding-agent/examples/extensions/dirty-repo-guard.ts index 6f259196..e6e2b5cc 100644 --- a/packages/coding-agent/examples/extensions/dirty-repo-guard.ts +++ b/packages/coding-agent/examples/extensions/dirty-repo-guard.ts @@ -50,7 +50,7 @@ export default function (pi: ExtensionAPI) { return checkDirtyRepo(pi, ctx, action); }); - pi.on("session_before_branch", async (_event, ctx) => { - return checkDirtyRepo(pi, ctx, "branch"); + pi.on("session_before_fork", async (_event, ctx) => { + return checkDirtyRepo(pi, ctx, "fork"); }); } diff --git a/packages/coding-agent/examples/extensions/git-checkpoint.ts b/packages/coding-agent/examples/extensions/git-checkpoint.ts index 7d0414bc..54ec6546 100644 --- a/packages/coding-agent/examples/extensions/git-checkpoint.ts +++ b/packages/coding-agent/examples/extensions/git-checkpoint.ts @@ -1,8 +1,8 @@ /** * Git Checkpoint Extension * - * Creates git stash checkpoints at each turn so /branch can restore code state. - * When branching, offers to restore code to that point in history. + * Creates git stash checkpoints at each turn so /fork can restore code state. + * When forking, offers to restore code to that point in history. */ 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); if (!ref) return; diff --git a/packages/coding-agent/examples/extensions/todo.ts b/packages/coding-agent/examples/extensions/todo.ts index 346ab93e..d55415f2 100644 --- a/packages/coding-agent/examples/extensions/todo.ts +++ b/packages/coding-agent/examples/extensions/todo.ts @@ -131,7 +131,7 @@ export default function (pi: ExtensionAPI) { // Reconstruct state on session events pi.on("session_start", 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)); // Register the todo tool for the LLM diff --git a/packages/coding-agent/examples/extensions/tools.ts b/packages/coding-agent/examples/extensions/tools.ts index dbd47377..7ba83dc5 100644 --- a/packages/coding-agent/examples/extensions/tools.ts +++ b/packages/coding-agent/examples/extensions/tools.ts @@ -138,8 +138,8 @@ export default function toolsExtension(pi: ExtensionAPI) { restoreFromBranch(ctx); }); - // Restore state after branching - pi.on("session_branch", async (_event, ctx) => { + // Restore state after forking + pi.on("session_fork", async (_event, ctx) => { restoreFromBranch(ctx); }); } diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 6969c9ca..d01d917f 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -37,8 +37,8 @@ import { import { exportSessionToHtml } from "./export-html/index.js"; import type { ExtensionRunner, - SessionBeforeBranchResult, SessionBeforeCompactResult, + SessionBeforeForkResult, SessionBeforeSwitchResult, SessionBeforeTreeResult, TreePreparation, @@ -1831,32 +1831,32 @@ export class AgentSession { } /** - * Create a branch from a specific entry. - * Emits before_branch/branch session events to extensions. + * Create a fork from a specific entry. + * 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: * - 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 selectedEntry = this.sessionManager.getEntry(entryId); 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); let skipConversationRestore = false; - // Emit session_before_branch event (can be cancelled) - if (this._extensionRunner?.hasHandlers("session_before_branch")) { + // Emit session_before_fork event (can be cancelled) + if (this._extensionRunner?.hasHandlers("session_before_fork")) { const result = (await this._extensionRunner.emit({ - type: "session_before_branch", + type: "session_before_fork", entryId, - })) as SessionBeforeBranchResult | undefined; + })) as SessionBeforeForkResult | undefined; if (result?.cancel) { return { selectedText, cancelled: true }; @@ -1877,15 +1877,15 @@ export class AgentSession { // Reload messages from entries (works for both file and in-memory mode) 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) { await this._extensionRunner.emit({ - type: "session_branch", + type: "session_fork", previousSessionFile, }); } - // Emit session event to custom tools (with reason "branch") + // Emit session event to custom tools (with reason "fork") if (!skipConversationRestore) { this.agent.replaceMessages(sessionContext.messages); @@ -1900,7 +1900,7 @@ export class AgentSession { /** * 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 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 result: Array<{ entryId: string; text: string }> = []; diff --git a/packages/coding-agent/src/core/extensions/index.ts b/packages/coding-agent/src/core/extensions/index.ts index 48c335e9..4b320a9a 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -9,8 +9,8 @@ export { loadExtensions, } from "./loader.js"; export type { - BranchHandler, ExtensionErrorListener, + ForkHandler, NavigateTreeHandler, NewSessionHandler, ShutdownHandler, @@ -75,17 +75,17 @@ export type { RegisteredTool, SendMessageHandler, SendUserMessageHandler, - SessionBeforeBranchEvent, - SessionBeforeBranchResult, SessionBeforeCompactEvent, SessionBeforeCompactResult, + SessionBeforeForkEvent, + SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, - SessionBranchEvent, SessionCompactEvent, SessionEvent, + SessionForkEvent, SessionShutdownEvent, // Events - Session SessionStartEvent, diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index bb75daac..73ba87dd 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -50,7 +50,7 @@ export type NewSessionHandler = (options?: { setup?: (sessionManager: SessionManager) => Promise; }) => Promise<{ cancelled: boolean }>; -export type BranchHandler = (entryId: string) => Promise<{ cancelled: boolean }>; +export type ForkHandler = (entryId: string) => Promise<{ cancelled: boolean }>; export type NavigateTreeHandler = ( targetId: string, @@ -111,7 +111,7 @@ export class ExtensionRunner { private abortFn: () => void = () => {}; private hasPendingMessagesFn: () => boolean = () => 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 shutdownHandler: ShutdownHandler = () => {}; @@ -158,7 +158,7 @@ export class ExtensionRunner { if (commandContextActions) { this.waitForIdleFn = commandContextActions.waitForIdle; this.newSessionHandler = commandContextActions.newSession; - this.branchHandler = commandContextActions.branch; + this.forkHandler = commandContextActions.fork; this.navigateTreeHandler = commandContextActions.navigateTree; } this.uiContext = uiContext ?? noOpUIContext; @@ -329,17 +329,17 @@ export class ExtensionRunner { ...this.createContext(), waitForIdle: () => this.waitForIdleFn(), newSession: (options) => this.newSessionHandler(options), - branch: (entryId) => this.branchHandler(entryId), + fork: (entryId) => this.forkHandler(entryId), navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options), }; } private isSessionBeforeEvent( 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 ( type === "session_before_switch" || - type === "session_before_branch" || + type === "session_before_fork" || type === "session_before_compact" || type === "session_before_tree" ); diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index ae8e8356..76cb8f90 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -218,8 +218,8 @@ export interface ExtensionCommandContext extends ExtensionContext { setup?: (sessionManager: SessionManager) => Promise; }): Promise<{ cancelled: boolean }>; - /** Branch from a specific entry, creating a new session file. */ - branch(entryId: string): Promise<{ cancelled: boolean }>; + /** Fork from a specific entry, creating a new session file. */ + fork(entryId: string): Promise<{ cancelled: boolean }>; /** Navigate to a different point in the session tree. */ navigateTree(targetId: string, options?: { summarize?: boolean }): Promise<{ cancelled: boolean }>; @@ -289,15 +289,15 @@ export interface SessionSwitchEvent { previousSessionFile: string | undefined; } -/** Fired before branching a session (can be cancelled) */ -export interface SessionBeforeBranchEvent { - type: "session_before_branch"; +/** Fired before forking a session (can be cancelled) */ +export interface SessionBeforeForkEvent { + type: "session_before_fork"; entryId: string; } -/** Fired after branching a session */ -export interface SessionBranchEvent { - type: "session_branch"; +/** Fired after forking a session */ +export interface SessionForkEvent { + type: "session_fork"; previousSessionFile: string | undefined; } @@ -351,8 +351,8 @@ export type SessionEvent = | SessionStartEvent | SessionBeforeSwitchEvent | SessionSwitchEvent - | SessionBeforeBranchEvent - | SessionBranchEvent + | SessionBeforeForkEvent + | SessionForkEvent | SessionBeforeCompactEvent | SessionCompactEvent | SessionShutdownEvent @@ -577,7 +577,7 @@ export interface SessionBeforeSwitchResult { cancel?: boolean; } -export interface SessionBeforeBranchResult { +export interface SessionBeforeForkResult { cancel?: boolean; skipConversationRestore?: boolean; } @@ -641,11 +641,8 @@ export interface ExtensionAPI { handler: ExtensionHandler, ): void; on(event: "session_switch", handler: ExtensionHandler): void; - on( - event: "session_before_branch", - handler: ExtensionHandler, - ): void; - on(event: "session_branch", handler: ExtensionHandler): void; + on(event: "session_before_fork", handler: ExtensionHandler): void; + on(event: "session_fork", handler: ExtensionHandler): void; on( event: "session_before_compact", handler: ExtensionHandler, @@ -858,7 +855,7 @@ export interface ExtensionCommandContextActions { parentSession?: string; setup?: (sessionManager: SessionManager) => Promise; }) => Promise<{ cancelled: boolean }>; - branch: (entryId: string) => Promise<{ cancelled: boolean }>; + fork: (entryId: string) => Promise<{ cancelled: boolean }>; navigateTree: (targetId: string, options?: { summarize?: boolean }) => Promise<{ cancelled: boolean }>; } diff --git a/packages/coding-agent/src/core/index.ts b/packages/coding-agent/src/core/index.ts index 5161194b..e84191d7 100644 --- a/packages/coding-agent/src/core/index.ts +++ b/packages/coding-agent/src/core/index.ts @@ -41,12 +41,12 @@ export { type LoadExtensionsResult, type MessageRenderer, type RegisteredCommand, - type SessionBeforeBranchEvent, type SessionBeforeCompactEvent, + type SessionBeforeForkEvent, type SessionBeforeSwitchEvent, type SessionBeforeTreeEvent, - type SessionBranchEvent, type SessionCompactEvent, + type SessionForkEvent, type SessionShutdownEvent, type SessionStartEvent, type SessionSwitchEvent, diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 7166a012..a844101d 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -66,7 +66,7 @@ export interface Settings { terminal?: TerminalSettings; images?: ImageSettings; 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 } @@ -452,11 +452,11 @@ export class SettingsManager { this.save(); } - getDoubleEscapeAction(): "branch" | "tree" { + getDoubleEscapeAction(): "fork" | "tree" { return this.settings.doubleEscapeAction ?? "tree"; } - setDoubleEscapeAction(action: "branch" | "tree"): void { + setDoubleEscapeAction(action: "fork" | "tree"): void { this.globalSettings.doubleEscapeAction = action; this.save(); } diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index ac9c49db..ae34b088 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -67,12 +67,12 @@ export type { MessageRenderOptions, RegisteredCommand, RegisteredTool, - SessionBeforeBranchEvent, SessionBeforeCompactEvent, + SessionBeforeForkEvent, SessionBeforeSwitchEvent, SessionBeforeTreeEvent, - SessionBranchEvent, SessionCompactEvent, + SessionForkEvent, SessionShutdownEvent, SessionStartEvent, SessionSwitchEvent, diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index b017fd6d..35eed241 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -35,7 +35,7 @@ export interface SettingsConfig { availableThemes: string[]; hideThinkingBlock: boolean; collapseChangelog: boolean; - doubleEscapeAction: "branch" | "tree"; + doubleEscapeAction: "fork" | "tree"; } export interface SettingsCallbacks { @@ -51,7 +51,7 @@ export interface SettingsCallbacks { onThemePreview?: (theme: string) => void; onHideThinkingBlockChange: (hidden: boolean) => void; onCollapseChangelogChange: (collapsed: boolean) => void; - onDoubleEscapeActionChange: (action: "branch" | "tree") => void; + onDoubleEscapeActionChange: (action: "fork" | "tree") => void; onCancel: () => void; } @@ -171,7 +171,7 @@ export class SettingsSelectorComponent extends Container { label: "Double-escape action", description: "Action when pressing Escape twice with empty editor", currentValue: config.doubleEscapeAction, - values: ["tree", "branch"], + values: ["tree", "fork"], }, { id: "thinking", @@ -304,7 +304,7 @@ export class SettingsSelectorComponent extends Container { callbacks.onCollapseChangelogChange(newValue === "true"); break; case "double-escape-action": - callbacks.onDoubleEscapeActionChange(newValue as "branch" | "tree"); + callbacks.onDoubleEscapeActionChange(newValue as "fork" | "tree"); break; } }, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index cd9e49c2..fcfba4b7 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -289,7 +289,7 @@ export class InteractiveMode { { name: "session", description: "Show session info and stats" }, { name: "changelog", description: "Show changelog entries" }, { 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: "login", description: "Login with OAuth provider" }, { name: "logout", description: "Logout from OAuth provider" }, @@ -730,8 +730,8 @@ export class InteractiveMode { return { cancelled: false }; }, - branch: async (entryId) => { - const result = await this.session.branch(entryId); + fork: async (entryId) => { + const result = await this.session.fork(entryId); if (result.cancelled) { return { cancelled: true }; } @@ -739,7 +739,7 @@ export class InteractiveMode { this.chatContainer.clear(); this.renderInitialMessages(); this.editor.setText(result.selectedText); - this.showStatus("Branched to new session"); + this.showStatus("Forked to new session"); return { cancelled: false }; }, @@ -1336,7 +1336,7 @@ export class InteractiveMode { this.isBashMode = false; this.updateEditorBorderColor(); } 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(); if (now - this.lastEscapeTime < 500) { if (this.settingsManager.getDoubleEscapeAction() === "tree") { @@ -1456,7 +1456,7 @@ export class InteractiveMode { this.editor.setText(""); return; } - if (text === "/branch") { + if (text === "/fork") { this.showUserMessageSelector(); this.editor.setText(""); return; @@ -2738,10 +2738,10 @@ export class InteractiveMode { } private showUserMessageSelector(): void { - const userMessages = this.session.getUserMessagesForBranching(); + const userMessages = this.session.getUserMessagesForForking(); if (userMessages.length === 0) { - this.showStatus("No messages to branch from"); + this.showStatus("No messages to fork from"); return; } @@ -2749,9 +2749,9 @@ export class InteractiveMode { const selector = new UserMessageSelectorComponent( userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => { - const result = await this.session.branch(entryId); + const result = await this.session.fork(entryId); if (result.cancelled) { - // Extension cancelled the branch + // Extension cancelled the fork done(); this.ui.requestRender(); return; diff --git a/packages/coding-agent/src/modes/print-mode.ts b/packages/coding-agent/src/modes/print-mode.ts index a7061b9f..f93a2f54 100644 --- a/packages/coding-agent/src/modes/print-mode.ts +++ b/packages/coding-agent/src/modes/print-mode.ts @@ -78,8 +78,8 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti } return { cancelled: !success }; }, - branch: async (entryId) => { - const result = await session.branch(entryId); + fork: async (entryId) => { + const result = await session.fork(entryId); return { cancelled: result.cancelled }; }, navigateTree: async (targetId, options) => { diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 6269a96c..739fd949 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -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) */ - async branch(entryId: string): Promise<{ text: string; cancelled: boolean }> { - const response = await this.send({ type: "branch", entryId }); + async fork(entryId: string): Promise<{ text: string; cancelled: boolean }> { + const response = await this.send({ type: "fork", entryId }); return this.getData(response); } /** - * Get messages available for branching. + * Get messages available for forking. */ - async getBranchMessages(): Promise> { - const response = await this.send({ type: "get_branch_messages" }); + async getForkMessages(): Promise> { + const response = await this.send({ type: "get_fork_messages" }); return this.getData<{ messages: Array<{ entryId: string; text: string }> }>(response).messages; } diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 8fecbeef..a0337df7 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -300,8 +300,8 @@ export async function runRpcMode(session: AgentSession): Promise { } return { cancelled: !success }; }, - branch: async (entryId) => { - const result = await session.branch(entryId); + fork: async (entryId) => { + const result = await session.fork(entryId); return { cancelled: result.cancelled }; }, navigateTree: async (targetId, options) => { @@ -508,14 +508,14 @@ export async function runRpcMode(session: AgentSession): Promise { return success(id, "switch_session", { cancelled }); } - case "branch": { - const result = await session.branch(command.entryId); - return success(id, "branch", { text: result.selectedText, cancelled: result.cancelled }); + case "fork": { + const result = await session.fork(command.entryId); + return success(id, "fork", { text: result.selectedText, cancelled: result.cancelled }); } - case "get_branch_messages": { - const messages = session.getUserMessagesForBranching(); - return success(id, "get_branch_messages", { messages }); + case "get_fork_messages": { + const messages = session.getUserMessagesForForking(); + return success(id, "get_fork_messages", { messages }); } case "get_last_assistant_text": { diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index f8bc0c59..ba173e12 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -55,8 +55,8 @@ export type RpcCommand = | { id?: string; type: "get_session_stats" } | { id?: string; type: "export_html"; outputPath?: string } | { id?: string; type: "switch_session"; sessionPath: string } - | { id?: string; type: "branch"; entryId: string } - | { id?: string; type: "get_branch_messages" } + | { id?: string; type: "fork"; entryId: string } + | { id?: string; type: "get_fork_messages" } | { id?: string; type: "get_last_assistant_text" } // 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: "export_html"; success: true; data: { path: string } } | { 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; type: "response"; - command: "get_branch_messages"; + command: "get_fork_messages"; success: true; data: { messages: Array<{ entryId: string; text: string }> }; } diff --git a/packages/coding-agent/test/agent-session-branching.test.ts b/packages/coding-agent/test/agent-session-branching.test.ts index 33f70853..c6028e19 100644 --- a/packages/coding-agent/test/agent-session-branching.test.ts +++ b/packages/coding-agent/test/agent-session-branching.test.ts @@ -1,10 +1,10 @@ /** - * Tests for AgentSession branching behavior. + * Tests for AgentSession forking behavior. * * These tests verify: - * - Branching from a single message works - * - Branching in --no-session mode (in-memory only) - * - getUserMessagesForBranching returns correct entries + * - Forking from a single message works + * - Forking in --no-session mode (in-memory only) + * - getUserMessagesForForking returns correct entries */ 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 { API_KEY } from "./utilities.js"; -describe.skipIf(!API_KEY)("AgentSession branching", () => { +describe.skipIf(!API_KEY)("AgentSession forking", () => { let session: AgentSession; let tempDir: string; let sessionManager: SessionManager; @@ -70,32 +70,32 @@ describe.skipIf(!API_KEY)("AgentSession branching", () => { return session; } - it("should allow branching from single message", async () => { + it("should allow forking from single message", async () => { createSession(); // Send one message await session.prompt("Say hello"); await session.agent.waitForIdle(); - // Should have exactly 1 user message available for branching - const userMessages = session.getUserMessagesForBranching(); + // Should have exactly 1 user message available for forking + const userMessages = session.getUserMessagesForForking(); expect(userMessages.length).toBe(1); expect(userMessages[0].text).toBe("Say hello"); - // Branch from the first message - const result = await session.branch(userMessages[0].entryId); + // Fork from the first message + const result = await session.fork(userMessages[0].entryId); expect(result.selectedText).toBe("Say hello"); 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); - // Session file should exist (new branch) + // Session file should exist (new fork) expect(session.sessionFile).not.toBeNull(); 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); // Verify sessions are disabled @@ -106,25 +106,25 @@ describe.skipIf(!API_KEY)("AgentSession branching", () => { await session.agent.waitForIdle(); // Should have 1 user message - const userMessages = session.getUserMessagesForBranching(); + const userMessages = session.getUserMessagesForForking(); expect(userMessages.length).toBe(1); - // Verify we have messages before branching + // Verify we have messages before forking expect(session.messages.length).toBeGreaterThan(0); - // Branch from the first message - const result = await session.branch(userMessages[0].entryId); + // Fork from the first message + const result = await session.fork(userMessages[0].entryId); expect(result.selectedText).toBe("Say hi"); expect(result.cancelled).toBe(false); - // After branching, conversation should be empty + // After forking, conversation should be empty expect(session.messages.length).toBe(0); // Session file should still be undefined (no file created) expect(session.sessionFile).toBeUndefined(); }); - it("should branch from middle of conversation", async () => { + it("should fork from middle of conversation", async () => { createSession(); // Send multiple messages @@ -138,15 +138,15 @@ describe.skipIf(!API_KEY)("AgentSession branching", () => { await session.agent.waitForIdle(); // Should have 3 user messages - const userMessages = session.getUserMessagesForBranching(); + const userMessages = session.getUserMessagesForForking(); 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 result = await session.branch(secondMessage.entryId); + const result = await session.fork(secondMessage.entryId); 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[0].role).toBe("user"); expect(session.messages[1].role).toBe("assistant");