feat: display hook execution in chat

This commit is contained in:
Toray Altas 2026-01-17 00:09:32 -05:00
parent 8bb5b61555
commit 08b41f2b9e
4 changed files with 71 additions and 4 deletions

View file

@ -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

View file

@ -541,8 +541,12 @@ export class Task extends EventEmitter<TaskEvents> 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

View file

@ -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<void>
/**
* 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<void> {
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)
}

View file

@ -1364,6 +1364,25 @@ export const ChatRowContent = ({
checkpoint={message.checkpoint}
/>
)
case "hook_triggered":
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "4px 0",
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
<span
className="codicon codicon-symbol-event"
style={{ fontSize: "14px" }}
aria-label="Hook icon"
/>
<span>Hook: {message.text} triggered</span>
</div>
)
case "condense_context":
// In-progress state
if (message.partial) {