From c56db60ee83e8567dd570df48558a569f49a928b Mon Sep 17 00:00:00 2001 From: rafia-10 Date: Sat, 21 Feb 2026 09:09:10 +0300 Subject: [PATCH] wrap each tool call with hooks --- .../presentAssistantMessage.ts | 160 +++++++++++++----- src/core/task/Task.ts | 4 - 2 files changed, 115 insertions(+), 49 deletions(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 7f5862be15..18abe77b4a 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -13,7 +13,8 @@ import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../sha import { AskIgnoredError } from "../task/AskIgnoredError" import { Task } from "../task/Task" - +import { preWriteHook } from "../../hooks/preHook" +import { postWriteHook } from "../../hooks/postHook" import { listFilesTool } from "../tools/ListFilesTool" import { readFileTool } from "../tools/ReadFileTool" import { readCommandOutputTool } from "../tools/ReadCommandOutputTool" @@ -58,6 +59,57 @@ import { sanitizeToolUseId } from "../../utils/tool-id" * as it becomes available. */ +async function runWithHooks( + cline: Task, + block: ToolUse, + handler: ToolHandler, + options?: { checkpoint?: boolean }, +) { + if (options?.checkpoint) await checkpointSaveAndMark(cline) + + // pre-hook can access cline and block + await preWriteHook(cline, block) + + // Keep helpers in scope so handlers still compile + const askApproval = async ( + type: ClineAsk, + partialMessage?: string, + progressStatus?: ToolProgressStatus, + isProtected?: boolean, + ) => { + const { response, text, images } = await cline.ask( + type, + partialMessage, + false, + progressStatus, + isProtected || false, + ) + if (response !== "yesButtonClicked") { + if (text) { + await cline.say("user_feedback", text, images) + pushToolResult(formatResponse.toolDeniedWithFeedback(text), images) + } else { + pushToolResult(formatResponse.toolDenied()) + } + return false + } + return true + } + + const pushToolResult = (content: ToolResponse, images?: string[]) => { + cline.pushToolResultToUserContent({ type: "tool_result", tool_use_id: block.id, content }) + if (images) cline.userMessageContent.push(...images) + } + + try { + await handler.handle(cline, block, { askApproval, handleError, pushToolResult }) + await postWriteHook(cline, block, { success: true }) + } catch (err) { + await postWriteHook(cline, block, { success: false, error: err }) + throw err + } +} + export async function presentAssistantMessage(cline: Task) { if (cline.abort) { throw new Error(`[Task#presentAssistantMessage] task ${cline.taskId}.${cline.instanceId} aborted`) @@ -326,63 +378,81 @@ export async function presentAssistantMessage(cline: Task) { const toolDescription = (): string => { switch (block.name) { - case "execute_command": - return `[${block.name} for '${block.params.command}']` - case "read_file": - // Prefer native typed args when available; fall back to legacy params - // Check if nativeArgs exists (native protocol) - if (block.nativeArgs) { - return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs) - } - return readFileTool.getReadFileToolDescription(block.name, block.params) case "write_to_file": - return `[${block.name} for '${block.params.path}']` + await runWithHooks(cline, block as ToolUse<"write_to_file">, writeToFileTool, { + checkpoint: true, + }) + break + case "update_todo_list": + await runWithHooks(cline, block as ToolUse<"update_todo_list">, updateTodoListTool) + break case "apply_diff": - // Native-only: tool args are structured (no XML payloads). - return block.params?.path ? `[${block.name} for '${block.params.path}']` : `[${block.name}]` - case "search_files": - return `[${block.name} for '${block.params.regex}'${ - block.params.file_pattern ? ` in '${block.params.file_pattern}'` : "" - }]` + await runWithHooks(cline, block as ToolUse<"apply_diff">, applyDiffToolClass, { + checkpoint: true, + }) + break case "edit": case "search_and_replace": - return `[${block.name} for '${block.params.file_path}']` + await runWithHooks(cline, block as ToolUse<"edit">, editTool, { checkpoint: true }) + break case "search_replace": - return `[${block.name} for '${block.params.file_path}']` + await runWithHooks(cline, block as ToolUse<"search_replace">, searchReplaceTool, { + checkpoint: true, + }) + break case "edit_file": - return `[${block.name} for '${block.params.file_path}']` + await runWithHooks(cline, block as ToolUse<"edit_file">, editFileTool, { checkpoint: true }) + break case "apply_patch": - return `[${block.name}]` + await runWithHooks(cline, block as ToolUse<"apply_patch">, applyPatchTool, { checkpoint: true }) + break + case "read_file": + await runWithHooks(cline, block as ToolUse<"read_file">, readFileTool) + break case "list_files": - return `[${block.name} for '${block.params.path}']` - case "use_mcp_tool": - return `[${block.name} for '${block.params.server_name}']` - case "access_mcp_resource": - return `[${block.name} for '${block.params.server_name}']` - case "ask_followup_question": - return `[${block.name} for '${block.params.question}']` - case "attempt_completion": - return `[${block.name}]` - case "switch_mode": - return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]` + await runWithHooks(cline, block as ToolUse<"list_files">, listFilesTool) + break case "codebase_search": - return `[${block.name} for '${block.params.query}']` + await runWithHooks(cline, block as ToolUse<"codebase_search">, codebaseSearchTool) + break + case "search_files": + await runWithHooks(cline, block as ToolUse<"search_files">, searchFilesTool) + break + case "execute_command": + await runWithHooks(cline, block as ToolUse<"execute_command">, executeCommandTool) + break case "read_command_output": - return `[${block.name} for '${block.params.artifact_id}']` - case "update_todo_list": - return `[${block.name}]` - case "new_task": { - const mode = block.params.mode ?? defaultModeSlug - const message = block.params.message ?? "(no message)" - const modeName = getModeBySlug(mode, customModes)?.name ?? mode - return `[${block.name} in ${modeName} mode: '${message}']` - } + await runWithHooks(cline, block as ToolUse<"read_command_output">, readCommandOutputTool) + break + case "use_mcp_tool": + await runWithHooks(cline, block as ToolUse<"use_mcp_tool">, useMcpToolTool) + break + case "access_mcp_resource": + await runWithHooks(cline, block as ToolUse<"access_mcp_resource">, accessMcpResourceTool) + break + case "ask_followup_question": + await runWithHooks(cline, block as ToolUse<"ask_followup_question">, askFollowupQuestionTool) + break + case "switch_mode": + await runWithHooks(cline, block as ToolUse<"switch_mode">, switchModeTool) + break + case "new_task": + await runWithHooks(cline, block as ToolUse<"new_task">, newTaskTool, { checkpoint: true }) + break + case "attempt_completion": + await runWithHooks(cline, block as ToolUse<"attempt_completion">, attemptCompletionTool) + break case "run_slash_command": - return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]` + await runWithHooks(cline, block as ToolUse<"run_slash_command">, runSlashCommandTool) + break case "skill": - return `[${block.name} for '${block.params.skill}'${block.params.args ? ` with args: ${block.params.args}` : ""}]` + await runWithHooks(cline, block as ToolUse<"skill">, skillTool) + break case "generate_image": - return `[${block.name} for '${block.params.path}']` + await runWithHooks(cline, block as ToolUse<"generate_image">, generateImageTool, { + checkpoint: true, + }) + break default: return `[${block.name}]` } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index aefaf3504f..5f693f915d 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -177,10 +177,6 @@ export class Task extends EventEmitter implements TaskLike { readonly taskNumber: number readonly workspacePath: string - // 1. The Math: Creating the Content Hash - private generateContentHash(content: string): string { - return crypto.createHash("sha256").update(content).digest("hex") - } /** * The mode associated with this task. Persisted across sessions * to maintain user context when reopening tasks from history.