From 08b41f2b9ed180d6f97354b4c80849a3f7f0a866 Mon Sep 17 00:00:00 2001 From: Toray Altas Date: Sat, 17 Jan 2026 00:09:32 -0500 Subject: [PATCH] feat: display hook execution in chat --- packages/types/src/message.ts | 2 + src/core/task/Task.ts | 8 +++- src/services/hooks/ToolExecutionHooks.ts | 46 +++++++++++++++++++++- webview-ui/src/components/chat/ChatRow.tsx | 19 +++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index 109cd842ba..e2371c9182 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -149,6 +149,7 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk { * - `condense_context`: Context condensation/summarization has started * - `condense_context_error`: Error occurred during context condensation * - `codebase_search_result`: Results from searching the codebase + * - `hook_triggered`: Notification that a hook has been executed */ export const clineSays = [ "error", @@ -179,6 +180,7 @@ export const clineSays = [ "condense_context_error", "sliding_window_truncation", "codebase_search_result", + "hook_triggered", "user_edit_todos", ] as const diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 07f8a6d808..64abfab687 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -541,8 +541,12 @@ export class Task extends EventEmitter implements TaskLike { }) // Initialize tool execution hooks - this.toolExecutionHooks = createToolExecutionHooks(provider.getHookManager() ?? null, (status) => - provider.postHookStatusToWebview(status), + this.toolExecutionHooks = createToolExecutionHooks( + provider.getHookManager() ?? null, + (status) => provider.postHookStatusToWebview(status), + async (type, text) => { + await this.say(type as ClineSay, text) + }, ) this.diffEnabled = enableDiff diff --git a/src/services/hooks/ToolExecutionHooks.ts b/src/services/hooks/ToolExecutionHooks.ts index 2db99abfce..ab1ee8da23 100644 --- a/src/services/hooks/ToolExecutionHooks.ts +++ b/src/services/hooks/ToolExecutionHooks.ts @@ -70,6 +70,11 @@ export type HookStatusCallback = (status: { modified?: boolean }) => void +/** + * Callback for emitting messages to chat history. + */ +export type SayCallback = (type: string, text?: string) => Promise + /** * Tool Execution Hooks Service * @@ -78,10 +83,12 @@ export type HookStatusCallback = (status: { export class ToolExecutionHooks { private hookManager: IHookManager | null private statusCallback?: HookStatusCallback + private sayCallback?: SayCallback - constructor(hookManager: IHookManager | null, statusCallback?: HookStatusCallback) { + constructor(hookManager: IHookManager | null, statusCallback?: HookStatusCallback, sayCallback?: SayCallback) { this.hookManager = hookManager this.statusCallback = statusCallback + this.sayCallback = sayCallback } /** @@ -98,6 +105,13 @@ export class ToolExecutionHooks { this.statusCallback = callback } + /** + * Update the say callback. + */ + setSayCallback(callback: SayCallback | undefined): void { + this.sayCallback = callback + } + /** * Execute PreToolUse hooks before a tool is executed. * @@ -128,6 +142,8 @@ export class ToolExecutionHooks { try { const result = await this.hookManager.executeHooks("PreToolUse", { context: hookContext }) + await this.emitHookTriggeredMessages(result) + if (result.blocked) { // Hook blocked the execution this.emitStatus({ @@ -216,6 +232,8 @@ export class ToolExecutionHooks { try { const result = await this.hookManager.executeHooks("PostToolUse", { context: hookContext }) + await this.emitHookTriggeredMessages(result) + this.emitStatus({ status: "completed", event: "PostToolUse", @@ -273,6 +291,8 @@ export class ToolExecutionHooks { try { const result = await this.hookManager.executeHooks("PostToolUseFailure", { context: hookContext }) + await this.emitHookTriggeredMessages(result) + this.emitStatus({ status: "completed", event: "PostToolUseFailure", @@ -331,6 +351,8 @@ export class ToolExecutionHooks { try { const result = await this.hookManager.executeHooks("PermissionRequest", { context: hookContext }) + await this.emitHookTriggeredMessages(result) + if (result.blocked) { // Hook blocked - do not show approval dialog, deny the tool this.emitStatus({ @@ -444,6 +466,25 @@ export class ToolExecutionHooks { } } } + + /** + * Emit hook triggered messages for successful hook executions. + */ + private async emitHookTriggeredMessages(result: HooksExecutionResult): Promise { + if (!this.sayCallback) { + return + } + + for (const hookResult of result.results) { + if (!hookResult.error && hookResult.exitCode === 0) { + try { + await this.sayCallback("hook_triggered", hookResult.hook.id) + } catch { + // Ignore callback errors + } + } + } + } } /** @@ -452,6 +493,7 @@ export class ToolExecutionHooks { export function createToolExecutionHooks( hookManager: IHookManager | null, statusCallback?: HookStatusCallback, + sayCallback?: SayCallback, ): ToolExecutionHooks { - return new ToolExecutionHooks(hookManager, statusCallback) + return new ToolExecutionHooks(hookManager, statusCallback, sayCallback) } diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 24749bb419..3ff934780c 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1364,6 +1364,25 @@ export const ChatRowContent = ({ checkpoint={message.checkpoint} /> ) + case "hook_triggered": + return ( +
+ + Hook: {message.text} triggered +
+ ) case "condense_context": // In-progress state if (message.partial) {