Update plan: HookCommandContext without sendMessage (use pi closure)

This commit is contained in:
Mario Zechner 2025-12-27 02:16:25 +01:00
parent 60130a4c53
commit 09e7e9196c
5 changed files with 22 additions and 27 deletions

View file

@ -165,20 +165,20 @@ Calls `sessionManager.appendCustomEntry()` directly.
**New: `registerCommand()` (types ✅, wiring TODO)**
```typescript
interface CommandContext {
interface HookCommandContext {
args: string; // Everything after /commandname
ui: HookUIContext;
hasUI: boolean;
cwd: string;
sessionManager: SessionManager;
modelRegistry: ModelRegistry;
sendMessage: HookAPI['sendMessage'];
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
}
// Note: sendMessage not on context - handler captures `pi` in closure
registerCommand(name: string, options: {
description?: string;
handler: (ctx: CommandContext) => Promise<string | undefined>;
handler: (ctx: HookCommandContext) => Promise<void>;
}): void;
```
@ -188,7 +188,7 @@ Handler return:
Wiring (all in AgentSession.prompt()):
- [x] Add hook commands to autocomplete in interactive-mode
- [x] `_tryExecuteHookCommand()` in AgentSession handles command execution
- [x] Build CommandContext with ui (from hookRunner), exec, sessionManager, etc.
- [x] Build HookCommandContext with ui (from hookRunner), exec, sessionManager, etc.
- [x] If handler returns string, use as prompt text
- [x] If handler returns undefined, return early (no LLM call)
- [x] Works for all modes (interactive, RPC, print) via shared AgentSession
@ -243,7 +243,7 @@ Review and update all docs:
- New `pi.appendEntry()` for state persistence
- New `pi.registerCommand()` for custom slash commands
- New `pi.registerCustomMessageRenderer()` for custom TUI rendering
- `CommandContext` interface and handler patterns
- `HookCommandContext` interface and handler patterns
- `HookMessage<T>` type
- Updated event signatures (`SessionEventBase`, `before_compact`, etc.)
- [ ] `docs/hooks-v2.md` - Review/merge or remove if obsolete

View file

@ -28,9 +28,9 @@ import {
import type { LoadedCustomTool, SessionEvent as ToolSessionEvent } from "./custom-tools/index.js";
import { exportSessionToHtml } from "./export-html.js";
import {
type CommandContext,
type ExecOptions,
execCommand,
type HookCommandContext,
type HookMessage,
type HookRunner,
type SessionEventResult,
@ -512,18 +512,13 @@ export class AgentSession {
// Build command context
const cwd = process.cwd();
const ctx: CommandContext = {
const ctx: HookCommandContext = {
args,
ui: uiContext,
hasUI: this._hookRunner.getHasUI(),
cwd,
sessionManager: this.sessionManager,
modelRegistry: this._modelRegistry,
sendMessage: (message, triggerTurn) => {
this.sendHookMessage(message, triggerTurn).catch(() => {
// Error handling is done in sendHookMessage
});
},
exec: (cmd: string, cmdArgs: string[], options?: ExecOptions) => execCommand(cmd, cmdArgs, cwd, options),
};

View file

@ -1,18 +1,18 @@
// biome-ignore assist/source/organizeImports: biome is not smart
export {
type AppendEntryHandler,
discoverAndLoadHooks,
loadHooks,
type AppendEntryHandler,
type LoadedHook,
type LoadHooksResult,
loadHooks,
type SendMessageHandler,
} from "./loader.js";
export { execCommand, type HookErrorListener, HookRunner } from "./runner.js";
export { execCommand, HookRunner, type HookErrorListener } from "./runner.js";
export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js";
export type {
AgentEndEvent,
AgentStartEvent,
BashToolResultEvent,
CommandContext,
CustomMessageRenderer,
CustomMessageRenderOptions,
CustomToolResultEvent,
@ -22,6 +22,7 @@ export type {
FindToolResultEvent,
GrepToolResultEvent,
HookAPI,
HookCommandContext,
HookError,
HookEvent,
HookEventContext,

View file

@ -12,6 +12,7 @@ import type { Theme } from "../../modes/interactive/theme/theme.js";
import type { CompactionPreparation, CompactionResult } from "../compaction.js";
import type { ModelRegistry } from "../model-registry.js";
import type { CompactionEntry, CustomMessageEntry, SessionManager } from "../session-manager.js";
import type { EditToolDetails } from "../tools/edit.js";
import type {
BashToolDetails,
FindToolDetails,
@ -237,7 +238,7 @@ export interface ReadToolResultEvent extends ToolResultEventBase {
/** Tool result event for edit tool */
export interface EditToolResultEvent extends ToolResultEventBase {
toolName: "edit";
details: undefined;
details: EditToolDetails | undefined;
}
/** Tool result event for write tool */
@ -394,9 +395,9 @@ export type CustomMessageRenderer<T = unknown> = (
export type HookMessage<T = unknown> = Pick<CustomMessageEntry<T>, "customType" | "content" | "display" | "details">;
/**
* Context passed to command handlers.
* Context passed to hook command handlers.
*/
export interface CommandContext {
export interface HookCommandContext {
/** Arguments after the command name */
args: string;
/** UI methods for user interaction */
@ -411,13 +412,6 @@ export interface CommandContext {
sessionManager: SessionManager;
/** Model registry for API keys */
modelRegistry: ModelRegistry;
/**
* Send a custom message to the session.
* If streaming, queued and appended after turn ends.
* If idle and triggerTurn=true, appends and triggers a new turn.
* If idle and triggerTurn=false (default), just appends.
*/
sendMessage<T = unknown>(message: HookMessage<T>, triggerTurn?: boolean): void;
}
/**
@ -426,7 +420,7 @@ export interface CommandContext {
export interface RegisteredCommand {
name: string;
description?: string;
handler: (ctx: CommandContext) => Promise<void>;
handler: (ctx: HookCommandContext) => Promise<void>;
}
/**

View file

@ -122,6 +122,11 @@ const editSchema = Type.Object({
newText: Type.String({ description: "New text to replace the old text with" }),
});
export interface EditToolDetails {
/** Unified diff of the changes made */
diff: string;
}
export function createEditTool(cwd: string): AgentTool<typeof editSchema> {
return {
name: "edit",