Add session management and agent state methods to hooks API

HookAPI additions:
- pi.newSession(options?) - create new session with optional setup callback
- pi.branch(entryId) - branch from a specific entry
- pi.navigateTree(targetId, options?) - navigate the session tree

HookContext additions:
- ctx.isIdle() - check if agent is streaming
- ctx.waitForIdle() - wait for agent to finish
- ctx.abort() - abort current operation
- ctx.hasQueuedMessages() - check for queued user messages

These enable hooks to programmatically manage sessions (handoff, templates)
and check agent state before showing interactive UI.

Fixes #388
This commit is contained in:
Mario Zechner 2026-01-01 23:56:24 +01:00
parent 484d7e06bb
commit ccdd7bd283
9 changed files with 355 additions and 14 deletions

View file

@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url";
import { createJiti } from "jiti";
import { getAgentDir } from "../../config.js";
import type { HookMessage } from "../messages.js";
import type { SessionManager } from "../session-manager.js";
import { execCommand } from "./runner.js";
import type { ExecOptions, HookAPI, HookFactory, HookMessageRenderer, RegisteredCommand } from "./types.js";
@ -60,6 +61,27 @@ export type SendMessageHandler = <T = unknown>(
*/
export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
/**
* New session handler type for pi.newSession().
*/
export type NewSessionHandler = (options?: {
parentSession?: string;
setup?: (sessionManager: SessionManager) => Promise<void>;
}) => Promise<{ cancelled: boolean }>;
/**
* Branch handler type for pi.branch().
*/
export type BranchHandler = (entryId: string) => Promise<{ cancelled: boolean }>;
/**
* Navigate tree handler type for pi.navigateTree().
*/
export type NavigateTreeHandler = (
targetId: string,
options?: { summarize?: boolean },
) => Promise<{ cancelled: boolean }>;
/**
* Registered handlers for a loaded hook.
*/
@ -78,6 +100,12 @@ export interface LoadedHook {
setSendMessageHandler: (handler: SendMessageHandler) => void;
/** Set the append entry handler for this hook's pi.appendEntry() */
setAppendEntryHandler: (handler: AppendEntryHandler) => void;
/** Set the new session handler for this hook's pi.newSession() */
setNewSessionHandler: (handler: NewSessionHandler) => void;
/** Set the branch handler for this hook's pi.branch() */
setBranchHandler: (handler: BranchHandler) => void;
/** Set the navigate tree handler for this hook's pi.navigateTree() */
setNavigateTreeHandler: (handler: NavigateTreeHandler) => void;
}
/**
@ -126,7 +154,7 @@ function resolveHookPath(hookPath: string, cwd: string): string {
/**
* Create a HookAPI instance that collects handlers, renderers, and commands.
* Returns the API, maps, and a function to set the send message handler later.
* Returns the API, maps, and functions to set handlers later.
*/
function createHookAPI(
handlers: Map<string, HandlerFn[]>,
@ -137,6 +165,9 @@ function createHookAPI(
commands: Map<string, RegisteredCommand>;
setSendMessageHandler: (handler: SendMessageHandler) => void;
setAppendEntryHandler: (handler: AppendEntryHandler) => void;
setNewSessionHandler: (handler: NewSessionHandler) => void;
setBranchHandler: (handler: BranchHandler) => void;
setNavigateTreeHandler: (handler: NavigateTreeHandler) => void;
} {
let sendMessageHandler: SendMessageHandler = () => {
// Default no-op until mode sets the handler
@ -144,6 +175,18 @@ function createHookAPI(
let appendEntryHandler: AppendEntryHandler = () => {
// Default no-op until mode sets the handler
};
let newSessionHandler: NewSessionHandler = async () => {
// Default no-op until mode sets the handler
return { cancelled: false };
};
let branchHandler: BranchHandler = async () => {
// Default no-op until mode sets the handler
return { cancelled: false };
};
let navigateTreeHandler: NavigateTreeHandler = async () => {
// Default no-op until mode sets the handler
return { cancelled: false };
};
const messageRenderers = new Map<string, HookMessageRenderer>();
const commands = new Map<string, RegisteredCommand>();
@ -170,6 +213,15 @@ function createHookAPI(
exec(command: string, args: string[], options?: ExecOptions) {
return execCommand(command, args, options?.cwd ?? cwd, options);
},
newSession(options) {
return newSessionHandler(options);
},
branch(entryId) {
return branchHandler(entryId);
},
navigateTree(targetId, options) {
return navigateTreeHandler(targetId, options);
},
} as HookAPI;
return {
@ -182,6 +234,15 @@ function createHookAPI(
setAppendEntryHandler: (handler: AppendEntryHandler) => {
appendEntryHandler = handler;
},
setNewSessionHandler: (handler: NewSessionHandler) => {
newSessionHandler = handler;
},
setBranchHandler: (handler: BranchHandler) => {
branchHandler = handler;
},
setNavigateTreeHandler: (handler: NavigateTreeHandler) => {
navigateTreeHandler = handler;
},
};
}
@ -209,10 +270,16 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
// Create handlers map and API
const handlers = new Map<string, HandlerFn[]>();
const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = createHookAPI(
handlers,
cwd,
);
const {
api,
messageRenderers,
commands,
setSendMessageHandler,
setAppendEntryHandler,
setNewSessionHandler,
setBranchHandler,
setNavigateTreeHandler,
} = createHookAPI(handlers, cwd);
// Call factory to register handlers
factory(api);
@ -226,6 +293,9 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
commands,
setSendMessageHandler,
setAppendEntryHandler,
setNewSessionHandler,
setBranchHandler,
setNavigateTreeHandler,
},
error: null,
};