Add before/after session events with cancellation support

- Merge branch event into session with before_branch/branch reasons
- Add before_switch, before_clear, shutdown reasons
- before_* events can be cancelled with { cancel: true }
- Update RPC commands to return cancelled status
- Add shutdown event on process exit
- New example hooks: confirm-destructive, dirty-repo-guard, auto-commit-on-exit

fixes #278
This commit is contained in:
Mario Zechner 2025-12-22 18:18:38 +01:00
parent 99081fce30
commit 42d7d9d9b6
20 changed files with 426 additions and 124 deletions

View file

@ -186,9 +186,11 @@ export class RpcClient {
/**
* Reset session (clear all messages).
* @returns Object with `cancelled: true` if a hook cancelled the reset
*/
async reset(): Promise<void> {
await this.send({ type: "reset" });
async reset(): Promise<{ cancelled: boolean }> {
const response = await this.send({ type: "reset" });
return this.getData(response);
}
/**
@ -311,15 +313,18 @@ export class RpcClient {
/**
* Switch to a different session file.
* @returns Object with `cancelled: true` if a hook cancelled the switch
*/
async switchSession(sessionPath: string): Promise<void> {
await this.send({ type: "switch_session", sessionPath });
async switchSession(sessionPath: string): Promise<{ cancelled: boolean }> {
const response = await this.send({ type: "switch_session", sessionPath });
return this.getData(response);
}
/**
* Branch from a specific message.
* @returns Object with `text` (the message text) and `cancelled` (if hook cancelled)
*/
async branch(entryIndex: number): Promise<{ text: string }> {
async branch(entryIndex: number): Promise<{ text: string; cancelled: boolean }> {
const response = await this.send({ type: "branch", entryIndex });
return this.getData(response);
}

View file

@ -205,8 +205,8 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
}
case "reset": {
await session.reset();
return success(id, "reset");
const cancelled = !(await session.reset());
return success(id, "reset", { cancelled });
}
// =================================================================
@ -339,13 +339,13 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
}
case "switch_session": {
await session.switchSession(command.sessionPath);
return success(id, "switch_session");
const cancelled = !(await session.switchSession(command.sessionPath));
return success(id, "switch_session", { cancelled });
}
case "branch": {
const result = await session.branch(command.entryIndex);
return success(id, "branch", { text: result.selectedText, skipped: result.skipped });
return success(id, "branch", { text: result.selectedText, cancelled: result.cancelled });
}
case "get_branch_messages": {

View file

@ -86,7 +86,7 @@ export type RpcResponse =
| { id?: string; type: "response"; command: "prompt"; success: true }
| { id?: string; type: "response"; command: "queue_message"; success: true }
| { id?: string; type: "response"; command: "abort"; success: true }
| { id?: string; type: "response"; command: "reset"; success: true }
| { id?: string; type: "response"; command: "reset"; success: true; data: { cancelled: boolean } }
// State
| { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState }
@ -142,8 +142,8 @@ export type RpcResponse =
// Session
| { 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 }
| { id?: string; type: "response"; command: "branch"; success: true; data: { text: 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";