mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-19 14:01:15 +00:00
Hook API: replace send() with sendMessage(), add appendEntry() and registerCommand()
Breaking changes to Hook API: - pi.send(text, attachments?) replaced with pi.sendMessage(message, triggerTurn?) - Creates CustomMessageEntry instead of user messages - Properly handles queuing during streaming via agent loop - Supports optional turn triggering when idle - New pi.appendEntry(customType, data?) for hook state persistence - New pi.registerCommand(name, options) for custom slash commands - Handler types renamed: SendHandler -> SendMessageHandler, new AppendEntryHandler Implementation: - AgentSession.sendHookMessage() handles all three cases: - Streaming: queues message with _hookData marker, agent loop processes it - Not streaming + triggerTurn: appends to state/session, calls agent.continue() - Not streaming + no trigger: appends to state/session only - message_end handler routes based on _hookData presence to correct persistence - HookRunner gains getRegisteredCommands() and getCommand() methods New types: HookMessage<T>, RegisteredCommand, CommandContext
This commit is contained in:
parent
d43a5e47a1
commit
ba185b0571
13 changed files with 412 additions and 77 deletions
|
|
@ -28,9 +28,14 @@
|
||||||
- Removed `resolveApiKey` (use `modelRegistry.getApiKey(model)`)
|
- Removed `resolveApiKey` (use `modelRegistry.getApiKey(model)`)
|
||||||
- Hooks can return `compaction.details` to store custom data (e.g., ArtifactIndex for structured compaction)
|
- Hooks can return `compaction.details` to store custom data (e.g., ArtifactIndex for structured compaction)
|
||||||
- **Hook API**:
|
- **Hook API**:
|
||||||
- New `pi.renderCustomMessage(customType, renderer)` to register custom renderers for `CustomMessageEntry`
|
- `pi.send(text, attachments?)` replaced with `pi.sendMessage(message, triggerTurn?)` which creates `CustomMessageEntry` instead of user messages
|
||||||
|
- New `pi.appendEntry(customType, data?)` to persist hook state (does NOT participate in LLM context)
|
||||||
|
- New `pi.registerCommand(name, options)` to register custom slash commands
|
||||||
|
- New `pi.registerCustomMessageRenderer(customType, renderer)` to register custom renderers for `CustomMessageEntry`
|
||||||
- `CustomMessageRenderer` type: `(entry, options, theme) => Component | null`
|
- `CustomMessageRenderer` type: `(entry, options, theme) => Component | null`
|
||||||
- Renderers return inner content; the TUI wraps it in a styled Box
|
- Renderers return inner content; the TUI wraps it in a styled Box
|
||||||
|
- New types: `HookMessage<T>`, `RegisteredCommand`, `CommandContext`
|
||||||
|
- Handler types renamed: `SendHandler` → `SendMessageHandler`, new `AppendEntryHandler`
|
||||||
- **SessionManager**:
|
- **SessionManager**:
|
||||||
- `getSessionFile()` now returns `string | undefined` (undefined for in-memory sessions)
|
- `getSessionFile()` now returns `string | undefined` (undefined for in-memory sessions)
|
||||||
- **Themes**: Custom themes must add `customMessageBg`, `customMessageText`, `customMessageLabel` color tokens
|
- **Themes**: Custom themes must add `customMessageBg`, `customMessageText`, `customMessageLabel` color tokens
|
||||||
|
|
@ -46,6 +51,7 @@
|
||||||
- **New entry types**: `BranchSummaryEntry` for branch context, `CustomEntry<T>` for hook state persistence, `CustomMessageEntry<T>` for hook-injected context messages, `LabelEntry` for user-defined bookmarks
|
- **New entry types**: `BranchSummaryEntry` for branch context, `CustomEntry<T>` for hook state persistence, `CustomMessageEntry<T>` for hook-injected context messages, `LabelEntry` for user-defined bookmarks
|
||||||
- **Entry labels**: New `getLabel(id)` and `appendLabelChange(targetId, label)` methods for labeling entries. Labels are included in `SessionTreeNode` for UI/export.
|
- **Entry labels**: New `getLabel(id)` and `appendLabelChange(targetId, label)` methods for labeling entries. Labels are included in `SessionTreeNode` for UI/export.
|
||||||
- **TUI**: `CustomMessageEntry` renders with purple styling (customMessageBg, customMessageText, customMessageLabel theme colors). Entries with `display: false` are hidden.
|
- **TUI**: `CustomMessageEntry` renders with purple styling (customMessageBg, customMessageText, customMessageLabel theme colors). Entries with `display: false` are hidden.
|
||||||
|
- **AgentSession**: New `sendHookMessage(message, triggerTurn?)` method for hooks to inject messages. Handles queuing during streaming, direct append when idle, and optional turn triggering.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,10 +122,67 @@ Behavior:
|
||||||
- [x] `appendCustomMessageEntry(customType, content, display, details?)` in SessionManager
|
- [x] `appendCustomMessageEntry(customType, content, display, details?)` in SessionManager
|
||||||
- [x] `buildSessionContext()` includes custom_message entries as user messages
|
- [x] `buildSessionContext()` includes custom_message entries as user messages
|
||||||
- [x] Exported from main index
|
- [x] Exported from main index
|
||||||
- [ ] TUI rendering:
|
- [x] TUI rendering:
|
||||||
- `display: false` - hidden entirely
|
- `display: false` - hidden entirely
|
||||||
- `display: true` - baseline renderer (content with different bg/fg color)
|
- `display: true` - rendered with purple styling (customMessageBg, customMessageText, customMessageLabel theme colors)
|
||||||
- Custom renderer defined by the hook that contributes it (future)
|
- [x] `registerCustomMessageRenderer(customType, renderer)` in HookAPI for custom renderers
|
||||||
|
- [x] Renderer returns inner Component, TUI wraps in styled Box
|
||||||
|
|
||||||
|
### Hook API Changes
|
||||||
|
|
||||||
|
**Renamed:**
|
||||||
|
- `renderCustomMessage()` → `registerCustomMessageRenderer()`
|
||||||
|
|
||||||
|
**New: `sendMessage()`**
|
||||||
|
|
||||||
|
Replaces `send()`. Always creates CustomMessageEntry, never user messages.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type HookMessage<T = unknown> = Pick<CustomMessageEntry<T>, 'customType' | 'content' | 'display' | 'details'>;
|
||||||
|
|
||||||
|
sendMessage(message: HookMessage, triggerTurn?: boolean): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- If streaming → queue, append after turn ends (never triggers turn)
|
||||||
|
- If idle AND `triggerTurn: true` → append and trigger turn
|
||||||
|
- If idle AND `triggerTurn: false` (default) → just append, no turn
|
||||||
|
- TUI updates if `display: true`
|
||||||
|
|
||||||
|
For hook state (CustomEntry), use `sessionManager.appendCustomEntry()` directly.
|
||||||
|
|
||||||
|
**New: `registerCommand()`**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface CommandContext {
|
||||||
|
args: string; // Everything after /commandname
|
||||||
|
session: LimitedAgentSession; // No prompt(), use sendMessage()
|
||||||
|
ui: HookUIContext;
|
||||||
|
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerCommand(name: string, options: {
|
||||||
|
description?: string;
|
||||||
|
handler: (ctx: CommandContext) => Promise<string | void>;
|
||||||
|
}): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
Handler return:
|
||||||
|
- `void` - command completed
|
||||||
|
- `string` - text to send as prompt (like file-based slash commands)
|
||||||
|
|
||||||
|
**New: `ui.custom()`**
|
||||||
|
|
||||||
|
For arbitrary hook UI with keyboard focus:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface HookUIContext {
|
||||||
|
// ... existing: select, confirm, input, notify
|
||||||
|
|
||||||
|
/** Show custom component with keyboard focus. Call done() when finished. */
|
||||||
|
custom(component: Component, done: () => void): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
See also: `CustomEntry<T>` for storing hook state that does NOT participate in context.
|
See also: `CustomEntry<T>` for storing hook state that does NOT participate in context.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,14 @@ export default function (pi: HookAPI) {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(triggerFile, "utf-8").trim();
|
const content = fs.readFileSync(triggerFile, "utf-8").trim();
|
||||||
if (content) {
|
if (content) {
|
||||||
pi.send(`External trigger: ${content}`);
|
pi.sendMessage(
|
||||||
|
{
|
||||||
|
customType: "file-trigger",
|
||||||
|
content: `External trigger: ${content}`,
|
||||||
|
display: true,
|
||||||
|
},
|
||||||
|
true, // triggerTurn - get LLM to respond
|
||||||
|
);
|
||||||
fs.writeFileSync(triggerFile, ""); // Clear after reading
|
fs.writeFileSync(triggerFile, ""); // Clear after reading
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Agent, AgentEvent, AgentState, AppMessage, Attachment, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
import type { Agent, AgentEvent, AgentState, AppMessage, Attachment, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||||
import type { AssistantMessage, Message, Model, TextContent } from "@mariozechner/pi-ai";
|
import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@mariozechner/pi-ai";
|
||||||
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
|
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
|
||||||
import { getAuthPath } from "../config.js";
|
import { getAuthPath } from "../config.js";
|
||||||
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor.js";
|
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor.js";
|
||||||
|
|
@ -27,7 +27,7 @@ import {
|
||||||
} from "./compaction.js";
|
} from "./compaction.js";
|
||||||
import type { LoadedCustomTool, SessionEvent as ToolSessionEvent } from "./custom-tools/index.js";
|
import type { LoadedCustomTool, SessionEvent as ToolSessionEvent } from "./custom-tools/index.js";
|
||||||
import { exportSessionToHtml } from "./export-html.js";
|
import { exportSessionToHtml } from "./export-html.js";
|
||||||
import type { HookRunner, SessionEventResult, TurnEndEvent, TurnStartEvent } from "./hooks/index.js";
|
import type { HookMessage, HookRunner, SessionEventResult, TurnEndEvent, TurnStartEvent } from "./hooks/index.js";
|
||||||
import type { BashExecutionMessage } from "./messages.js";
|
import type { BashExecutionMessage } from "./messages.js";
|
||||||
import type { ModelRegistry } from "./model-registry.js";
|
import type { ModelRegistry } from "./model-registry.js";
|
||||||
import type { CompactionEntry, SessionManager } from "./session-manager.js";
|
import type { CompactionEntry, SessionManager } from "./session-manager.js";
|
||||||
|
|
@ -101,6 +101,13 @@ export interface SessionStats {
|
||||||
cost: number;
|
cost: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Internal marker for hook messages queued through the agent loop */
|
||||||
|
interface HookMessageData {
|
||||||
|
customType: string;
|
||||||
|
display: boolean;
|
||||||
|
details?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Constants
|
// Constants
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -211,7 +218,21 @@ export class AgentSession {
|
||||||
|
|
||||||
// Handle session persistence
|
// Handle session persistence
|
||||||
if (event.type === "message_end") {
|
if (event.type === "message_end") {
|
||||||
this.sessionManager.appendMessage(event.message);
|
// Check if this is a hook message (has _hookData marker)
|
||||||
|
type HookAppMessage = AppMessage & { _hookData?: HookMessageData; content: (TextContent | ImageContent)[] };
|
||||||
|
const hookMessage = event.message as HookAppMessage;
|
||||||
|
if (hookMessage._hookData) {
|
||||||
|
// Persist as CustomMessageEntry
|
||||||
|
this.sessionManager.appendCustomMessageEntry(
|
||||||
|
hookMessage._hookData.customType,
|
||||||
|
hookMessage.content,
|
||||||
|
hookMessage._hookData.display,
|
||||||
|
hookMessage._hookData.details,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Regular message - persist as SessionMessageEntry
|
||||||
|
this.sessionManager.appendMessage(event.message);
|
||||||
|
}
|
||||||
|
|
||||||
// Track assistant message for auto-compaction (checked on agent_end)
|
// Track assistant message for auto-compaction (checked on agent_end)
|
||||||
if (event.message.role === "assistant") {
|
if (event.message.role === "assistant") {
|
||||||
|
|
@ -473,6 +494,60 @@ export class AgentSession {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a hook message to the session. Creates a CustomMessageEntry.
|
||||||
|
*
|
||||||
|
* Handles three cases:
|
||||||
|
* - Streaming: queues message, processed when loop pulls from queue
|
||||||
|
* - Not streaming + triggerTurn: appends to state/session, starts new turn
|
||||||
|
* - Not streaming + no trigger: appends to state/session, no turn
|
||||||
|
*
|
||||||
|
* @param message Hook message with customType, content, display, details
|
||||||
|
* @param triggerTurn If true and not streaming, triggers a new LLM turn
|
||||||
|
*/
|
||||||
|
async sendHookMessage<T = unknown>(message: HookMessage<T>, triggerTurn?: boolean): Promise<void> {
|
||||||
|
// Normalize content to array format for the AppMessage
|
||||||
|
const content: (TextContent | ImageContent)[] =
|
||||||
|
typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content;
|
||||||
|
|
||||||
|
// Create AppMessage with _hookData marker for routing in message_end handler
|
||||||
|
const appMessage: AppMessage & { _hookData: HookMessageData } = {
|
||||||
|
role: "user",
|
||||||
|
content,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
_hookData: {
|
||||||
|
customType: message.customType,
|
||||||
|
display: message.display,
|
||||||
|
details: message.details,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.isStreaming) {
|
||||||
|
// Queue for processing by agent loop
|
||||||
|
await this.agent.queueMessage(appMessage);
|
||||||
|
} else if (triggerTurn) {
|
||||||
|
// Append to agent state and session, then trigger a turn
|
||||||
|
this.agent.appendMessage(appMessage);
|
||||||
|
this.sessionManager.appendCustomMessageEntry(
|
||||||
|
message.customType,
|
||||||
|
message.content,
|
||||||
|
message.display,
|
||||||
|
message.details,
|
||||||
|
);
|
||||||
|
// Start a new turn - agent.continue() works because last message is user role
|
||||||
|
await this.agent.continue();
|
||||||
|
} else {
|
||||||
|
// Just append to agent state and session, no turn
|
||||||
|
this.agent.appendMessage(appMessage);
|
||||||
|
this.sessionManager.appendCustomMessageEntry(
|
||||||
|
message.customType,
|
||||||
|
message.content,
|
||||||
|
message.display,
|
||||||
|
message.details,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear queued messages and return them.
|
* Clear queued messages and return them.
|
||||||
* Useful for restoring to editor when user aborts.
|
* Useful for restoring to editor when user aborts.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
export { discoverAndLoadHooks, type LoadedHook, type LoadHooksResult, loadHooks, type SendHandler } from "./loader.js";
|
export {
|
||||||
|
type AppendEntryHandler,
|
||||||
|
discoverAndLoadHooks,
|
||||||
|
type LoadedHook,
|
||||||
|
type LoadHooksResult,
|
||||||
|
loadHooks,
|
||||||
|
type SendMessageHandler,
|
||||||
|
} from "./loader.js";
|
||||||
export { type HookErrorListener, HookRunner } from "./runner.js";
|
export { type HookErrorListener, HookRunner } from "./runner.js";
|
||||||
export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js";
|
export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js";
|
||||||
export type {
|
export type {
|
||||||
|
|
@ -17,9 +24,11 @@ export type {
|
||||||
HookEvent,
|
HookEvent,
|
||||||
HookEventContext,
|
HookEventContext,
|
||||||
HookFactory,
|
HookFactory,
|
||||||
|
HookMessage,
|
||||||
HookUIContext,
|
HookUIContext,
|
||||||
LsToolResultEvent,
|
LsToolResultEvent,
|
||||||
ReadToolResultEvent,
|
ReadToolResultEvent,
|
||||||
|
RegisteredCommand,
|
||||||
SessionEvent,
|
SessionEvent,
|
||||||
SessionEventResult,
|
SessionEventResult,
|
||||||
ToolCallEvent,
|
ToolCallEvent,
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,9 @@ import { createRequire } from "node:module";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import type { Attachment } from "@mariozechner/pi-agent-core";
|
|
||||||
import { createJiti } from "jiti";
|
import { createJiti } from "jiti";
|
||||||
import { getAgentDir } from "../../config.js";
|
import { getAgentDir } from "../../config.js";
|
||||||
import type { CustomMessageRenderer, HookAPI, HookFactory } from "./types.js";
|
import type { CustomMessageRenderer, HookAPI, HookFactory, HookMessage, RegisteredCommand } from "./types.js";
|
||||||
|
|
||||||
// Create require function to resolve module paths at runtime
|
// Create require function to resolve module paths at runtime
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
|
|
@ -47,9 +46,14 @@ function getAliases(): Record<string, string> {
|
||||||
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send handler type for pi.send().
|
* Send message handler type for pi.sendMessage().
|
||||||
*/
|
*/
|
||||||
export type SendHandler = (text: string, attachments?: Attachment[]) => void;
|
export type SendMessageHandler = <T = unknown>(message: HookMessage<T>, triggerTurn?: boolean) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append entry handler type for pi.appendEntry().
|
||||||
|
*/
|
||||||
|
export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registered handlers for a loaded hook.
|
* Registered handlers for a loaded hook.
|
||||||
|
|
@ -63,8 +67,12 @@ export interface LoadedHook {
|
||||||
handlers: Map<string, HandlerFn[]>;
|
handlers: Map<string, HandlerFn[]>;
|
||||||
/** Map of customType to custom message renderer */
|
/** Map of customType to custom message renderer */
|
||||||
customMessageRenderers: Map<string, CustomMessageRenderer>;
|
customMessageRenderers: Map<string, CustomMessageRenderer>;
|
||||||
/** Set the send handler for this hook's pi.send() */
|
/** Map of command name to registered command */
|
||||||
setSendHandler: (handler: SendHandler) => void;
|
commands: Map<string, RegisteredCommand>;
|
||||||
|
/** Set the send message handler for this hook's pi.sendMessage() */
|
||||||
|
setSendMessageHandler: (handler: SendMessageHandler) => void;
|
||||||
|
/** Set the append entry handler for this hook's pi.appendEntry() */
|
||||||
|
setAppendEntryHandler: (handler: AppendEntryHandler) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -112,18 +120,24 @@ function resolveHookPath(hookPath: string, cwd: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a HookAPI instance that collects handlers and renderers.
|
* Create a HookAPI instance that collects handlers, renderers, and commands.
|
||||||
* Returns the API, renderers map, and a function to set the send handler later.
|
* Returns the API, maps, and a function to set the send message handler later.
|
||||||
*/
|
*/
|
||||||
function createHookAPI(handlers: Map<string, HandlerFn[]>): {
|
function createHookAPI(handlers: Map<string, HandlerFn[]>): {
|
||||||
api: HookAPI;
|
api: HookAPI;
|
||||||
customMessageRenderers: Map<string, CustomMessageRenderer>;
|
customMessageRenderers: Map<string, CustomMessageRenderer>;
|
||||||
setSendHandler: (handler: SendHandler) => void;
|
commands: Map<string, RegisteredCommand>;
|
||||||
|
setSendMessageHandler: (handler: SendMessageHandler) => void;
|
||||||
|
setAppendEntryHandler: (handler: AppendEntryHandler) => void;
|
||||||
} {
|
} {
|
||||||
let sendHandler: SendHandler = () => {
|
let sendMessageHandler: SendMessageHandler = () => {
|
||||||
|
// Default no-op until mode sets the handler
|
||||||
|
};
|
||||||
|
let appendEntryHandler: AppendEntryHandler = () => {
|
||||||
// Default no-op until mode sets the handler
|
// Default no-op until mode sets the handler
|
||||||
};
|
};
|
||||||
const customMessageRenderers = new Map<string, CustomMessageRenderer>();
|
const customMessageRenderers = new Map<string, CustomMessageRenderer>();
|
||||||
|
const commands = new Map<string, RegisteredCommand>();
|
||||||
|
|
||||||
const api: HookAPI = {
|
const api: HookAPI = {
|
||||||
on(event: string, handler: HandlerFn): void {
|
on(event: string, handler: HandlerFn): void {
|
||||||
|
|
@ -131,19 +145,29 @@ function createHookAPI(handlers: Map<string, HandlerFn[]>): {
|
||||||
list.push(handler);
|
list.push(handler);
|
||||||
handlers.set(event, list);
|
handlers.set(event, list);
|
||||||
},
|
},
|
||||||
send(text: string, attachments?: Attachment[]): void {
|
sendMessage<T = unknown>(message: HookMessage<T>, triggerTurn?: boolean): void {
|
||||||
sendHandler(text, attachments);
|
sendMessageHandler(message, triggerTurn);
|
||||||
},
|
},
|
||||||
renderCustomMessage(customType: string, renderer: CustomMessageRenderer): void {
|
appendEntry<T = unknown>(customType: string, data?: T): void {
|
||||||
|
appendEntryHandler(customType, data);
|
||||||
|
},
|
||||||
|
registerCustomMessageRenderer(customType: string, renderer: CustomMessageRenderer): void {
|
||||||
customMessageRenderers.set(customType, renderer);
|
customMessageRenderers.set(customType, renderer);
|
||||||
},
|
},
|
||||||
|
registerCommand(name: string, options: { description?: string; handler: RegisteredCommand["handler"] }): void {
|
||||||
|
commands.set(name, { name, ...options });
|
||||||
|
},
|
||||||
} as HookAPI;
|
} as HookAPI;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
api,
|
api,
|
||||||
customMessageRenderers,
|
customMessageRenderers,
|
||||||
setSendHandler: (handler: SendHandler) => {
|
commands,
|
||||||
sendHandler = handler;
|
setSendMessageHandler: (handler: SendMessageHandler) => {
|
||||||
|
sendMessageHandler = handler;
|
||||||
|
},
|
||||||
|
setAppendEntryHandler: (handler: AppendEntryHandler) => {
|
||||||
|
appendEntryHandler = handler;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -172,13 +196,22 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
|
||||||
|
|
||||||
// Create handlers map and API
|
// Create handlers map and API
|
||||||
const handlers = new Map<string, HandlerFn[]>();
|
const handlers = new Map<string, HandlerFn[]>();
|
||||||
const { api, customMessageRenderers, setSendHandler } = createHookAPI(handlers);
|
const { api, customMessageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } =
|
||||||
|
createHookAPI(handlers);
|
||||||
|
|
||||||
// Call factory to register handlers
|
// Call factory to register handlers
|
||||||
factory(api);
|
factory(api);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hook: { path: hookPath, resolvedPath, handlers, customMessageRenderers, setSendHandler },
|
hook: {
|
||||||
|
path: hookPath,
|
||||||
|
resolvedPath,
|
||||||
|
handlers,
|
||||||
|
customMessageRenderers,
|
||||||
|
commands,
|
||||||
|
setSendMessageHandler,
|
||||||
|
setAppendEntryHandler,
|
||||||
|
},
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import type { LoadedHook, SendHandler } from "./loader.js";
|
import type { AppendEntryHandler, LoadedHook, SendMessageHandler } from "./loader.js";
|
||||||
import type {
|
import type {
|
||||||
CustomMessageRenderer,
|
CustomMessageRenderer,
|
||||||
ExecOptions,
|
ExecOptions,
|
||||||
|
|
@ -12,6 +12,7 @@ import type {
|
||||||
HookEvent,
|
HookEvent,
|
||||||
HookEventContext,
|
HookEventContext,
|
||||||
HookUIContext,
|
HookUIContext,
|
||||||
|
RegisteredCommand,
|
||||||
SessionEvent,
|
SessionEvent,
|
||||||
SessionEventResult,
|
SessionEventResult,
|
||||||
ToolCallEvent,
|
ToolCallEvent,
|
||||||
|
|
@ -164,12 +165,22 @@ export class HookRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the send handler for all hooks' pi.send().
|
* Set the send message handler for all hooks' pi.sendMessage().
|
||||||
* Call this when the mode initializes.
|
* Call this when the mode initializes.
|
||||||
*/
|
*/
|
||||||
setSendHandler(handler: SendHandler): void {
|
setSendMessageHandler(handler: SendMessageHandler): void {
|
||||||
for (const hook of this.hooks) {
|
for (const hook of this.hooks) {
|
||||||
hook.setSendHandler(handler);
|
hook.setSendMessageHandler(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the append entry handler for all hooks' pi.appendEntry().
|
||||||
|
* Call this when the mode initializes.
|
||||||
|
*/
|
||||||
|
setAppendEntryHandler(handler: AppendEntryHandler): void {
|
||||||
|
for (const hook of this.hooks) {
|
||||||
|
hook.setAppendEntryHandler(handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,6 +229,33 @@ export class HookRunner {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered commands from all hooks.
|
||||||
|
*/
|
||||||
|
getRegisteredCommands(): RegisteredCommand[] {
|
||||||
|
const commands: RegisteredCommand[] = [];
|
||||||
|
for (const hook of this.hooks) {
|
||||||
|
for (const command of hook.commands.values()) {
|
||||||
|
commands.push(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a registered command by name.
|
||||||
|
* Returns the first command found across all hooks, or undefined if none.
|
||||||
|
*/
|
||||||
|
getCommand(name: string): RegisteredCommand | undefined {
|
||||||
|
for (const hook of this.hooks) {
|
||||||
|
const command = hook.commands.get(name);
|
||||||
|
if (command) {
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the event context for handlers.
|
* Create the event context for handlers.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* and interact with the user via UI primitives.
|
* and interact with the user via UI primitives.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AppMessage, Attachment } from "@mariozechner/pi-agent-core";
|
import type { AppMessage } from "@mariozechner/pi-agent-core";
|
||||||
import type { ImageContent, Model, TextContent, ToolResultMessage } from "@mariozechner/pi-ai";
|
import type { ImageContent, Model, TextContent, ToolResultMessage } from "@mariozechner/pi-ai";
|
||||||
import type { Component } from "@mariozechner/pi-tui";
|
import type { Component } from "@mariozechner/pi-tui";
|
||||||
import type { Theme } from "../../modes/interactive/theme/theme.js";
|
import type { Theme } from "../../modes/interactive/theme/theme.js";
|
||||||
|
|
@ -388,9 +388,50 @@ export type CustomMessageRenderer<T = unknown> = (
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
) => Component | null;
|
) => Component | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message type for hooks to send. Creates CustomMessageEntry in the session.
|
||||||
|
*/
|
||||||
|
export type HookMessage<T = unknown> = Pick<CustomMessageEntry<T>, "customType" | "content" | "display" | "details">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context passed to command handlers.
|
||||||
|
*/
|
||||||
|
export interface CommandContext {
|
||||||
|
/** Arguments after the command name */
|
||||||
|
args: string;
|
||||||
|
/** UI methods for user interaction */
|
||||||
|
ui: HookUIContext;
|
||||||
|
/** Execute a command and return stdout/stderr/code */
|
||||||
|
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
|
||||||
|
/** Whether UI is available (false in print mode) */
|
||||||
|
hasUI: boolean;
|
||||||
|
/** Current working directory */
|
||||||
|
cwd: string;
|
||||||
|
/** Session manager for reading/writing session entries */
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command registration options.
|
||||||
|
*/
|
||||||
|
export interface RegisteredCommand {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
handler: (ctx: CommandContext) => Promise<string | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HookAPI passed to hook factory functions.
|
* HookAPI passed to hook factory functions.
|
||||||
* Hooks use pi.on() to subscribe to events and pi.send() to inject messages.
|
* Hooks use pi.on() to subscribe to events and pi.sendMessage() to inject messages.
|
||||||
*/
|
*/
|
||||||
export interface HookAPI {
|
export interface HookAPI {
|
||||||
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
|
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
|
||||||
|
|
@ -403,18 +444,62 @@ export interface HookAPI {
|
||||||
on(event: "tool_result", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;
|
on(event: "tool_result", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a message to the agent.
|
* Send a custom message to the session. Creates a CustomMessageEntry that
|
||||||
* If the agent is streaming, the message is queued.
|
* participates in LLM context and can be displayed in the TUI.
|
||||||
* If the agent is idle, a new agent loop is started.
|
*
|
||||||
|
* Use this when you want the LLM to see the message content.
|
||||||
|
* For hook state that should NOT be sent to the LLM, use appendEntry() instead.
|
||||||
|
*
|
||||||
|
* @param message - The message to send
|
||||||
|
* @param message.customType - Identifier for your hook (used for filtering on reload)
|
||||||
|
* @param message.content - Message content (string or TextContent/ImageContent array)
|
||||||
|
* @param message.display - Whether to show in TUI (true = styled display, false = hidden)
|
||||||
|
* @param message.details - Optional hook-specific metadata (not sent to LLM)
|
||||||
|
* @param triggerTurn - If true and agent is idle, triggers a new LLM turn. Default: false.
|
||||||
|
* If agent is streaming, message is queued and triggerTurn is ignored.
|
||||||
*/
|
*/
|
||||||
send(text: string, attachments?: Attachment[]): void;
|
sendMessage<T = unknown>(message: HookMessage<T>, triggerTurn?: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a custom entry to the session for hook state persistence.
|
||||||
|
* Creates a CustomEntry that does NOT participate in LLM context.
|
||||||
|
*
|
||||||
|
* Use this to store hook-specific data that should persist across session reloads
|
||||||
|
* but should NOT be sent to the LLM. On reload, scan session entries for your
|
||||||
|
* customType to reconstruct hook state.
|
||||||
|
*
|
||||||
|
* For messages that SHOULD be sent to the LLM, use sendMessage() instead.
|
||||||
|
*
|
||||||
|
* @param customType - Identifier for your hook (used for filtering on reload)
|
||||||
|
* @param data - Hook-specific data to persist (must be JSON-serializable)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Store permission state
|
||||||
|
* pi.appendEntry("permissions", { level: "full", grantedAt: Date.now() });
|
||||||
|
*
|
||||||
|
* // On reload, reconstruct state from entries
|
||||||
|
* pi.on("session", async (event, ctx) => {
|
||||||
|
* if (event.reason === "start") {
|
||||||
|
* const entries = event.sessionManager.getEntries();
|
||||||
|
* const myEntries = entries.filter(e => e.type === "custom" && e.customType === "permissions");
|
||||||
|
* // Reconstruct state from myEntries...
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
appendEntry<T = unknown>(customType: string, data?: T): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a custom renderer for CustomMessageEntry with a specific customType.
|
* Register a custom renderer for CustomMessageEntry with a specific customType.
|
||||||
* The renderer is called when rendering the entry in the TUI.
|
* The renderer is called when rendering the entry in the TUI.
|
||||||
* Return null to use the default renderer.
|
* Return null to use the default renderer.
|
||||||
*/
|
*/
|
||||||
renderCustomMessage<T = unknown>(customType: string, renderer: CustomMessageRenderer<T>): void;
|
registerCustomMessageRenderer<T = unknown>(customType: string, renderer: CustomMessageRenderer<T>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a custom slash command.
|
||||||
|
* Handler receives CommandContext and can return a string to send as prompt.
|
||||||
|
*/
|
||||||
|
registerCommand(name: string, options: { description?: string; handler: RegisteredCommand["handler"] }): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -341,7 +341,9 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
|
||||||
return definitions.map((def) => {
|
return definitions.map((def) => {
|
||||||
const handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>();
|
const handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>();
|
||||||
const customMessageRenderers = new Map<string, any>();
|
const customMessageRenderers = new Map<string, any>();
|
||||||
let sendHandler: (text: string, attachments?: any[]) => void = () => {};
|
const commands = new Map<string, any>();
|
||||||
|
let sendMessageHandler: (message: any, triggerTurn?: boolean) => void = () => {};
|
||||||
|
let appendEntryHandler: (customType: string, data?: any) => void = () => {};
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
on: (event: string, handler: (...args: unknown[]) => Promise<unknown>) => {
|
on: (event: string, handler: (...args: unknown[]) => Promise<unknown>) => {
|
||||||
|
|
@ -349,12 +351,18 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
|
||||||
list.push(handler);
|
list.push(handler);
|
||||||
handlers.set(event, list);
|
handlers.set(event, list);
|
||||||
},
|
},
|
||||||
send: (text: string, attachments?: any[]) => {
|
sendMessage: (message: any, triggerTurn?: boolean) => {
|
||||||
sendHandler(text, attachments);
|
sendMessageHandler(message, triggerTurn);
|
||||||
},
|
},
|
||||||
renderCustomMessage: (customType: string, renderer: any) => {
|
appendEntry: (customType: string, data?: any) => {
|
||||||
|
appendEntryHandler(customType, data);
|
||||||
|
},
|
||||||
|
registerCustomMessageRenderer: (customType: string, renderer: any) => {
|
||||||
customMessageRenderers.set(customType, renderer);
|
customMessageRenderers.set(customType, renderer);
|
||||||
},
|
},
|
||||||
|
registerCommand: (name: string, options: any) => {
|
||||||
|
commands.set(name, { name, ...options });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
def.factory(api as any);
|
def.factory(api as any);
|
||||||
|
|
@ -364,8 +372,12 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
|
||||||
resolvedPath: def.path ?? "<inline>",
|
resolvedPath: def.path ?? "<inline>",
|
||||||
handlers,
|
handlers,
|
||||||
customMessageRenderers,
|
customMessageRenderers,
|
||||||
setSendHandler: (handler: (text: string, attachments?: any[]) => void) => {
|
commands,
|
||||||
sendHandler = handler;
|
setSendMessageHandler: (handler: (message: any, triggerTurn?: boolean) => void) => {
|
||||||
|
sendMessageHandler = handler;
|
||||||
|
},
|
||||||
|
setAppendEntryHandler: (handler: (customType: string, data?: any) => void) => {
|
||||||
|
appendEntryHandler = handler;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import type { AgentState, AppMessage, Attachment } from "@mariozechner/pi-agent-core";
|
import type { AgentState, AppMessage } from "@mariozechner/pi-agent-core";
|
||||||
import type { AssistantMessage, Message, OAuthProvider } from "@mariozechner/pi-ai";
|
import type { AssistantMessage, Message, OAuthProvider } from "@mariozechner/pi-ai";
|
||||||
import type { SlashCommand } from "@mariozechner/pi-tui";
|
import type { SlashCommand } from "@mariozechner/pi-tui";
|
||||||
import {
|
import {
|
||||||
|
|
@ -370,9 +370,24 @@ export class InteractiveMode {
|
||||||
this.showHookError(error.hookPath, error.error);
|
this.showHookError(error.hookPath, error.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up send handler for pi.send()
|
// Set up handlers for pi.sendMessage() and pi.appendEntry()
|
||||||
hookRunner.setSendHandler((text, attachments) => {
|
hookRunner.setSendMessageHandler((message, triggerTurn) => {
|
||||||
this.handleHookSend(text, attachments);
|
const wasStreaming = this.session.isStreaming;
|
||||||
|
this.session
|
||||||
|
.sendHookMessage(message, triggerTurn)
|
||||||
|
.then(() => {
|
||||||
|
// For non-streaming cases with display=true, update UI
|
||||||
|
// (streaming cases update via message_end event)
|
||||||
|
if (!wasStreaming && message.display) {
|
||||||
|
this.rebuildChatFromMessages();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.showError(`Hook sendMessage failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
hookRunner.setAppendEntryHandler((customType, data) => {
|
||||||
|
this.sessionManager.appendCustomEntry(customType, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show loaded hooks
|
// Show loaded hooks
|
||||||
|
|
@ -534,19 +549,6 @@ export class InteractiveMode {
|
||||||
* Handle pi.send() from hooks.
|
* Handle pi.send() from hooks.
|
||||||
* If streaming, queue the message. Otherwise, start a new agent loop.
|
* If streaming, queue the message. Otherwise, start a new agent loop.
|
||||||
*/
|
*/
|
||||||
private handleHookSend(text: string, attachments?: Attachment[]): void {
|
|
||||||
if (this.session.isStreaming) {
|
|
||||||
// Queue the message for later (note: attachments are lost when queuing)
|
|
||||||
this.session.queueMessage(text);
|
|
||||||
this.updatePendingMessagesDisplay();
|
|
||||||
} else {
|
|
||||||
// Start a new agent loop immediately
|
|
||||||
this.session.prompt(text, { attachments }).catch((err) => {
|
|
||||||
this.showError(err instanceof Error ? err.message : String(err));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Key Handlers
|
// Key Handlers
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,14 @@ export async function runPrintMode(
|
||||||
hookRunner.onError((err) => {
|
hookRunner.onError((err) => {
|
||||||
console.error(`Hook error (${err.hookPath}): ${err.error}`);
|
console.error(`Hook error (${err.hookPath}): ${err.error}`);
|
||||||
});
|
});
|
||||||
// No-op send handler for print mode (single-shot, no async messages)
|
// Set up handlers - sendHookMessage handles queuing/direct append as needed
|
||||||
hookRunner.setSendHandler(() => {
|
hookRunner.setSendMessageHandler((message, triggerTurn) => {
|
||||||
console.error("Warning: pi.send() is not supported in print mode");
|
session.sendHookMessage(message, triggerTurn).catch((e) => {
|
||||||
|
console.error(`Hook sendMessage failed: ${e instanceof Error ? e.message : String(e)}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
hookRunner.setAppendEntryHandler((customType, data) => {
|
||||||
|
session.sessionManager.appendCustomEntry(customType, data);
|
||||||
});
|
});
|
||||||
// Emit session event
|
// Emit session event
|
||||||
await hookRunner.emit({
|
await hookRunner.emit({
|
||||||
|
|
|
||||||
|
|
@ -131,16 +131,14 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
||||||
hookRunner.onError((err) => {
|
hookRunner.onError((err) => {
|
||||||
output({ type: "hook_error", hookPath: err.hookPath, event: err.event, error: err.error });
|
output({ type: "hook_error", hookPath: err.hookPath, event: err.event, error: err.error });
|
||||||
});
|
});
|
||||||
// Set up send handler for pi.send()
|
// Set up handlers for pi.sendMessage() and pi.appendEntry()
|
||||||
hookRunner.setSendHandler((text, attachments) => {
|
hookRunner.setSendMessageHandler((message, triggerTurn) => {
|
||||||
// In RPC mode, just queue or prompt based on streaming state
|
session.sendHookMessage(message, triggerTurn).catch((e) => {
|
||||||
if (session.isStreaming) {
|
output(error(undefined, "hook_send", e.message));
|
||||||
session.queueMessage(text);
|
});
|
||||||
} else {
|
});
|
||||||
session.prompt(text, { attachments }).catch((e) => {
|
hookRunner.setAppendEntryHandler((customType, data) => {
|
||||||
output(error(undefined, "hook_send", e.message));
|
session.sessionManager.appendCustomEntry(customType, data);
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// Emit session event
|
// Emit session event
|
||||||
await hookRunner.emit({
|
await hookRunner.emit({
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,9 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
||||||
resolvedPath: "/test/test-hook.ts",
|
resolvedPath: "/test/test-hook.ts",
|
||||||
handlers,
|
handlers,
|
||||||
customMessageRenderers: new Map(),
|
customMessageRenderers: new Map(),
|
||||||
setSendHandler: () => {},
|
commands: new Map(),
|
||||||
|
setSendMessageHandler: () => {},
|
||||||
|
setAppendEntryHandler: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,7 +242,9 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
customMessageRenderers: new Map(),
|
customMessageRenderers: new Map(),
|
||||||
setSendHandler: () => {},
|
commands: new Map(),
|
||||||
|
setSendMessageHandler: () => {},
|
||||||
|
setAppendEntryHandler: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
createSession([throwingHook]);
|
createSession([throwingHook]);
|
||||||
|
|
@ -284,7 +288,9 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
customMessageRenderers: new Map(),
|
customMessageRenderers: new Map(),
|
||||||
setSendHandler: () => {},
|
commands: new Map(),
|
||||||
|
setSendMessageHandler: () => {},
|
||||||
|
setAppendEntryHandler: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const hook2: LoadedHook = {
|
const hook2: LoadedHook = {
|
||||||
|
|
@ -307,7 +313,9 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
customMessageRenderers: new Map(),
|
customMessageRenderers: new Map(),
|
||||||
setSendHandler: () => {},
|
commands: new Map(),
|
||||||
|
setSendMessageHandler: () => {},
|
||||||
|
setAppendEntryHandler: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
createSession([hook1, hook2]);
|
createSession([hook1, hook2]);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue