mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 09:02:08 +00:00
Improve hooks.md UI documentation
- Add 'Key capabilities' section highlighting UI features - Expand ctx.ui docs with custom component details - Reference snake.ts example for custom UI
This commit is contained in:
parent
88e39471ea
commit
29e0ed9cd1
1 changed files with 42 additions and 21 deletions
|
|
@ -2,14 +2,21 @@
|
||||||
|
|
||||||
Hooks are TypeScript modules that extend pi's behavior by subscribing to lifecycle events. They can intercept tool calls, prompt the user, modify results, inject messages, and more.
|
Hooks are TypeScript modules that extend pi's behavior by subscribing to lifecycle events. They can intercept tool calls, prompt the user, modify results, inject messages, and more.
|
||||||
|
|
||||||
**Example use cases:**
|
**Key capabilities:**
|
||||||
- Block dangerous commands (permission gates for `rm -rf`, `sudo`)
|
- **User interaction** - Hooks can prompt users via `ctx.ui` (select, confirm, input, notify)
|
||||||
- Checkpoint code state (git stash at each turn, restore on branch)
|
- **Custom UI components** - Full TUI components with keyboard input via `ctx.ui.custom()`
|
||||||
- Protect paths (block writes to `.env`, `node_modules/`)
|
- **Custom slash commands** - Register commands like `/mycommand` via `pi.registerCommand()`
|
||||||
- Inject messages from external sources (file watchers, webhooks)
|
- **Event interception** - Block or modify tool calls, inject context, customize compaction
|
||||||
- Custom slash commands and UI components
|
- **Session persistence** - Store hook state that survives restarts via `pi.appendEntry()`
|
||||||
|
|
||||||
See [examples/hooks/](../examples/hooks/) for working implementations.
|
**Example use cases:**
|
||||||
|
- Permission gates (confirm before `rm -rf`, `sudo`, etc.)
|
||||||
|
- Git checkpointing (stash at each turn, restore on `/branch`)
|
||||||
|
- Path protection (block writes to `.env`, `node_modules/`)
|
||||||
|
- External integrations (file watchers, webhooks, CI triggers)
|
||||||
|
- Interactive tools (games, wizards, custom dialogs)
|
||||||
|
|
||||||
|
See [examples/hooks/](../examples/hooks/) for working implementations, including a [snake game](../examples/hooks/snake.ts) demonstrating custom UI.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|
@ -187,7 +194,7 @@ Fired when branching via `/branch`.
|
||||||
```typescript
|
```typescript
|
||||||
pi.on("session_before_branch", async (event, ctx) => {
|
pi.on("session_before_branch", async (event, ctx) => {
|
||||||
// event.entryIndex - entry index being branched from
|
// event.entryIndex - entry index being branched from
|
||||||
|
|
||||||
return { cancel: true }; // Cancel branch
|
return { cancel: true }; // Cancel branch
|
||||||
// OR
|
// OR
|
||||||
return { skipConversationRestore: true }; // Branch but don't rewind messages
|
return { skipConversationRestore: true }; // Branch but don't rewind messages
|
||||||
|
|
@ -207,10 +214,10 @@ Fired on compaction. See [compaction.md](compaction.md) for details.
|
||||||
```typescript
|
```typescript
|
||||||
pi.on("session_before_compact", async (event, ctx) => {
|
pi.on("session_before_compact", async (event, ctx) => {
|
||||||
const { preparation, branchEntries, customInstructions, signal } = event;
|
const { preparation, branchEntries, customInstructions, signal } = event;
|
||||||
|
|
||||||
// Cancel:
|
// Cancel:
|
||||||
return { cancel: true };
|
return { cancel: true };
|
||||||
|
|
||||||
// Custom summary:
|
// Custom summary:
|
||||||
return {
|
return {
|
||||||
compaction: {
|
compaction: {
|
||||||
|
|
@ -236,7 +243,7 @@ pi.on("session_before_tree", async (event, ctx) => {
|
||||||
const { preparation, signal } = event;
|
const { preparation, signal } = event;
|
||||||
// preparation.targetId, oldLeafId, commonAncestorId, entriesToSummarize
|
// preparation.targetId, oldLeafId, commonAncestorId, entriesToSummarize
|
||||||
// preparation.userWantsSummary - whether user chose to summarize
|
// preparation.userWantsSummary - whether user chose to summarize
|
||||||
|
|
||||||
return { cancel: true };
|
return { cancel: true };
|
||||||
// OR provide custom summary (only used if userWantsSummary is true):
|
// OR provide custom summary (only used if userWantsSummary is true):
|
||||||
return { summary: { summary: "...", details: {} } };
|
return { summary: { summary: "...", details: {} } };
|
||||||
|
|
@ -267,7 +274,7 @@ Fired after user submits prompt, before agent loop. Can inject a persistent mess
|
||||||
pi.on("before_agent_start", async (event, ctx) => {
|
pi.on("before_agent_start", async (event, ctx) => {
|
||||||
// event.prompt - user's prompt text
|
// event.prompt - user's prompt text
|
||||||
// event.images - attached images (if any)
|
// event.images - attached images (if any)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
customType: "my-hook",
|
customType: "my-hook",
|
||||||
|
|
@ -315,7 +322,7 @@ Fired before each LLM call. Modify messages non-destructively (session unchanged
|
||||||
```typescript
|
```typescript
|
||||||
pi.on("context", async (event, ctx) => {
|
pi.on("context", async (event, ctx) => {
|
||||||
// event.messages - deep copy, safe to modify
|
// event.messages - deep copy, safe to modify
|
||||||
|
|
||||||
// Filter or transform messages
|
// Filter or transform messages
|
||||||
const filtered = event.messages.filter(m => !shouldPrune(m));
|
const filtered = event.messages.filter(m => !shouldPrune(m));
|
||||||
return { messages: filtered };
|
return { messages: filtered };
|
||||||
|
|
@ -333,7 +340,7 @@ pi.on("tool_call", async (event, ctx) => {
|
||||||
// event.toolName - "bash", "read", "write", "edit", etc.
|
// event.toolName - "bash", "read", "write", "edit", etc.
|
||||||
// event.toolCallId
|
// event.toolCallId
|
||||||
// event.input - tool parameters
|
// event.input - tool parameters
|
||||||
|
|
||||||
if (shouldBlock(event)) {
|
if (shouldBlock(event)) {
|
||||||
return { block: true, reason: "Not allowed" };
|
return { block: true, reason: "Not allowed" };
|
||||||
}
|
}
|
||||||
|
|
@ -359,7 +366,7 @@ pi.on("tool_result", async (event, ctx) => {
|
||||||
// event.content - array of TextContent | ImageContent
|
// event.content - array of TextContent | ImageContent
|
||||||
// event.details - tool-specific (see below)
|
// event.details - tool-specific (see below)
|
||||||
// event.isError
|
// event.isError
|
||||||
|
|
||||||
// Modify result:
|
// Modify result:
|
||||||
return { content: [...], details: {...}, isError: false };
|
return { content: [...], details: {...}, isError: false };
|
||||||
});
|
});
|
||||||
|
|
@ -388,7 +395,9 @@ Every handler receives `ctx: HookContext`:
|
||||||
|
|
||||||
### ctx.ui
|
### ctx.ui
|
||||||
|
|
||||||
UI methods for user interaction:
|
UI methods for user interaction. Hooks can prompt users and even render custom TUI components.
|
||||||
|
|
||||||
|
**Built-in dialogs:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Select from options
|
// Select from options
|
||||||
|
|
@ -403,19 +412,31 @@ const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
|
||||||
const name = await ctx.ui.input("Name:", "placeholder");
|
const name = await ctx.ui.input("Name:", "placeholder");
|
||||||
// Returns string or undefined if cancelled
|
// Returns string or undefined if cancelled
|
||||||
|
|
||||||
// Notification
|
// Notification (non-blocking)
|
||||||
ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
|
ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
|
||||||
|
```
|
||||||
|
|
||||||
// Custom component with keyboard focus
|
**Custom components:**
|
||||||
|
|
||||||
|
For full control, render your own TUI component with keyboard focus:
|
||||||
|
|
||||||
|
```typescript
|
||||||
const handle = ctx.ui.custom(myComponent);
|
const handle = ctx.ui.custom(myComponent);
|
||||||
// Returns { close: () => void, requestRender: () => void }
|
// Returns { close: () => void, requestRender: () => void }
|
||||||
// Component can implement handleInput(data: string) for keyboard
|
|
||||||
// Call handle.close() when done
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Your component can:
|
||||||
|
- Implement `handleInput(data: string)` to receive keyboard input
|
||||||
|
- Implement `render(width: number): string[]` to render lines
|
||||||
|
- Implement `invalidate()` to clear cached render
|
||||||
|
- Call `handle.requestRender()` to trigger re-render
|
||||||
|
- Call `handle.close()` when done to restore normal UI
|
||||||
|
|
||||||
|
See [examples/hooks/snake.ts](../examples/hooks/snake.ts) for a complete example with game loop, keyboard handling, and state persistence.
|
||||||
|
|
||||||
### ctx.hasUI
|
### ctx.hasUI
|
||||||
|
|
||||||
`false` in print mode (`-p`) and RPC mode. Always check before using `ctx.ui`:
|
`false` in print mode (`-p`), JSON print mode, and RPC mode. Always check before using `ctx.ui`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
if (ctx.hasUI) {
|
if (ctx.hasUI) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue