Change branch() to use entryId instead of entryIndex

- AgentSession.branch(entryId: string) now takes entry ID
- SessionBeforeBranchEvent.entryId replaces entryIndex
- getUserMessagesForBranching() returns entryId
- Update RPC types and client
- Update UserMessageSelectorComponent
- Update hook examples and tests
- Update docs (hooks.md, sdk.md)
This commit is contained in:
Mario Zechner 2025-12-31 13:47:34 +01:00
parent 027d39aa33
commit 8e1e99ca05
12 changed files with 64 additions and 50 deletions

View file

@ -1498,21 +1498,20 @@ export class AgentSession {
}
/**
* Create a branch from a specific entry index.
* Create a branch from a specific entry.
* Emits before_branch/branch session events to hooks.
*
* @param entryIndex Index into session entries to branch from
* @param entryId ID of the entry to branch from
* @returns Object with:
* - selectedText: The text of the selected user message (for editor pre-fill)
* - cancelled: True if a hook cancelled the branch
*/
async branch(entryIndex: number): Promise<{ selectedText: string; cancelled: boolean }> {
async branch(entryId: string): Promise<{ selectedText: string; cancelled: boolean }> {
const previousSessionFile = this.sessionFile;
const entries = this.sessionManager.getEntries();
const selectedEntry = entries[entryIndex];
const selectedEntry = this.sessionManager.getEntry(entryId);
if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
throw new Error("Invalid entry index for branching");
throw new Error("Invalid entry ID for branching");
}
const selectedText = this._extractUserMessageText(selectedEntry.message.content);
@ -1523,7 +1522,7 @@ export class AgentSession {
if (this._hookRunner?.hasHandlers("session_before_branch")) {
const result = (await this._hookRunner.emit({
type: "session_before_branch",
entryIndex: entryIndex,
entryId,
})) as SessionBeforeBranchResult | undefined;
if (result?.cancel) {
@ -1729,18 +1728,17 @@ export class AgentSession {
/**
* Get all user messages from session for branch selector.
*/
getUserMessagesForBranching(): Array<{ entryIndex: number; text: string }> {
getUserMessagesForBranching(): Array<{ entryId: string; text: string }> {
const entries = this.sessionManager.getEntries();
const result: Array<{ entryIndex: number; text: string }> = [];
const result: Array<{ entryId: string; text: string }> = [];
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
for (const entry of entries) {
if (entry.type !== "message") continue;
if (entry.message.role !== "user") continue;
const text = this._extractUserMessageText(entry.message.content);
if (text) {
result.push({ entryIndex: i, text });
result.push({ entryId: entry.id, text });
}
}

View file

@ -121,8 +121,8 @@ export interface SessionNewEvent {
/** Fired before branching a session (can be cancelled) */
export interface SessionBeforeBranchEvent {
type: "session_before_branch";
/** Index of the entry in the session (SessionManager.getEntries()) to branch from */
entryIndex: number;
/** ID of the entry to branch from */
entryId: string;
}
/** Fired after branching a session */

View file

@ -14,7 +14,7 @@ import { theme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js";
interface UserMessageItem {
index: number; // Index in the full messages array
id: string; // Entry ID in the session
text: string; // The message text
timestamp?: string; // Optional timestamp if available
}
@ -25,7 +25,7 @@ interface UserMessageItem {
class UserMessageList implements Component {
private messages: UserMessageItem[] = [];
private selectedIndex: number = 0;
public onSelect?: (messageIndex: number) => void;
public onSelect?: (entryId: string) => void;
public onCancel?: () => void;
private maxVisible: number = 10; // Max messages visible
@ -101,7 +101,7 @@ class UserMessageList implements Component {
else if (isEnter(keyData)) {
const selected = this.messages[this.selectedIndex];
if (selected && this.onSelect) {
this.onSelect(selected.index);
this.onSelect(selected.id);
}
}
// Escape - cancel
@ -125,7 +125,7 @@ class UserMessageList implements Component {
export class UserMessageSelectorComponent extends Container {
private messageList: UserMessageList;
constructor(messages: UserMessageItem[], onSelect: (messageIndex: number) => void, onCancel: () => void) {
constructor(messages: UserMessageItem[], onSelect: (entryId: string) => void, onCancel: () => void) {
super();
// Add header

View file

@ -1570,9 +1570,9 @@ export class InteractiveMode {
this.showSelector((done) => {
const selector = new UserMessageSelectorComponent(
userMessages.map((m) => ({ index: m.entryIndex, text: m.text })),
async (entryIndex) => {
const result = await this.session.branch(entryIndex);
userMessages.map((m) => ({ id: m.entryId, text: m.text })),
async (entryId) => {
const result = await this.session.branch(entryId);
if (result.cancelled) {
// Hook cancelled the branch
done();

View file

@ -326,17 +326,17 @@ export class RpcClient {
* Branch from a specific message.
* @returns Object with `text` (the message text) and `cancelled` (if hook cancelled)
*/
async branch(entryIndex: number): Promise<{ text: string; cancelled: boolean }> {
const response = await this.send({ type: "branch", entryIndex });
async branch(entryId: string): Promise<{ text: string; cancelled: boolean }> {
const response = await this.send({ type: "branch", entryId });
return this.getData(response);
}
/**
* Get messages available for branching.
*/
async getBranchMessages(): Promise<Array<{ entryIndex: number; text: string }>> {
async getBranchMessages(): Promise<Array<{ entryId: string; text: string }>> {
const response = await this.send({ type: "get_branch_messages" });
return this.getData<{ messages: Array<{ entryIndex: number; text: string }> }>(response).messages;
return this.getData<{ messages: Array<{ entryId: string; text: string }> }>(response).messages;
}
/**

View file

@ -347,7 +347,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
}
case "branch": {
const result = await session.branch(command.entryIndex);
const result = await session.branch(command.entryId);
return success(id, "branch", { text: result.selectedText, cancelled: result.cancelled });
}

View file

@ -53,7 +53,7 @@ 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"; entryIndex: number }
| { id?: string; type: "branch"; entryId: string }
| { id?: string; type: "get_branch_messages" }
| { id?: string; type: "get_last_assistant_text" }
@ -150,7 +150,7 @@ export type RpcResponse =
type: "response";
command: "get_branch_messages";
success: true;
data: { messages: Array<{ entryIndex: number; text: string }> };
data: { messages: Array<{ entryId: string; text: string }> };
}
| {
id?: string;