mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b5b4092a4 | |||
| 993d2001c8 | |||
| bf92470b4c | |||
| ebcc766178 | |||
| cb8fa39bc5 |
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Error thrown when a PreToolUse hook requests cancellation.
|
||||
* This signals to the tool handler that execution should be aborted.
|
||||
*/
|
||||
export class PreToolUseHookCancellationError extends Error {
|
||||
constructor(message: string = "PreToolUse hook requested cancellation") {
|
||||
super(message)
|
||||
this.name = "PreToolUseHookCancellationError"
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,12 @@ export async function executeHook<Name extends keyof Hooks>(options: HookExecuti
|
||||
}
|
||||
hookMessageTs = await say("hook", JSON.stringify(hookMetadata))
|
||||
|
||||
// Reorder messages immediately so hook UI appears above tool UI
|
||||
// This must happen right after creating the hook message, before the hook runs
|
||||
if (hookName === "PreToolUse") {
|
||||
await reorderHookAndToolMessages(messageStateHandler)
|
||||
}
|
||||
|
||||
// Track active hook execution for cancellation (only if cancellable and message was created)
|
||||
if (isCancellable && hookMessageTs !== undefined && setActiveHookExecution) {
|
||||
await setActiveHookExecution({
|
||||
@@ -224,3 +230,47 @@ async function updateHookMessage(
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorders hook and tool messages so hook UI appears before tool UI.
|
||||
* This is called immediately after a hook message is created.
|
||||
*
|
||||
* The algorithm:
|
||||
* 1. Find the most recent tool message (ask or say with type "tool")
|
||||
* 2. Find any hook messages that came after it
|
||||
* 3. Delete the tool message
|
||||
* 4. Re-add the tool message at the end (after hook messages)
|
||||
*/
|
||||
async function reorderHookAndToolMessages(messageStateHandler: MessageStateHandler): Promise<void> {
|
||||
const { findLastIndex } = await import("@shared/array")
|
||||
const clineMessages = messageStateHandler.getClineMessages()
|
||||
|
||||
// Find the most recent tool message
|
||||
const lastToolMessageIndex = findLastIndex(clineMessages, (m: ClineMessage) => m.ask === "tool" || m.say === "tool")
|
||||
|
||||
if (lastToolMessageIndex === -1) {
|
||||
return // No tool message found, nothing to reorder
|
||||
}
|
||||
|
||||
// Check if there are any hook messages after the tool message
|
||||
let hasHookMessagesAfterTool = false
|
||||
for (let i = lastToolMessageIndex + 1; i < clineMessages.length; i++) {
|
||||
if (clineMessages[i].say === "hook" || clineMessages[i].say === "hook_output") {
|
||||
hasHookMessagesAfterTool = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasHookMessagesAfterTool) {
|
||||
return // No reordering needed
|
||||
}
|
||||
|
||||
// Store the tool message (deep copy to preserve all properties)
|
||||
const toolMessage = { ...clineMessages[lastToolMessageIndex] }
|
||||
|
||||
// Delete the tool message at its current position
|
||||
await messageStateHandler.deleteClineMessage(lastToolMessageIndex)
|
||||
|
||||
// Re-add the tool message at the end (after hook messages)
|
||||
await messageStateHandler.addToClineMessages(toolMessage)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { AssistantMessageContent } from "@core/assistant-message"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import type { HookExecution } from "./types/HookExecution"
|
||||
|
||||
export class TaskState {
|
||||
// Streaming flags
|
||||
@@ -62,12 +63,7 @@ export class TaskState {
|
||||
abandoned = false
|
||||
|
||||
// Hook execution tracking for cancellation
|
||||
activeHookExecution?: {
|
||||
hookName: string
|
||||
toolName?: string
|
||||
messageTs: number
|
||||
abortController: AbortController
|
||||
}
|
||||
activeHookExecution?: HookExecution
|
||||
|
||||
// Auto-context summarization
|
||||
currentlySummarizing: boolean = false
|
||||
|
||||
@@ -174,6 +174,9 @@ export class ToolExecutor {
|
||||
shouldAutoApproveToolWithPath: this.shouldAutoApproveToolWithPath.bind(this),
|
||||
applyLatestBrowserSettings: this.applyLatestBrowserSettings.bind(this),
|
||||
switchToActMode: this.switchToActMode,
|
||||
setActiveHookExecution: this.setActiveHookExecution,
|
||||
clearActiveHookExecution: this.clearActiveHookExecution,
|
||||
getActiveHookExecution: this.getActiveHookExecution,
|
||||
},
|
||||
coordinator: this.coordinator,
|
||||
}
|
||||
@@ -528,14 +531,15 @@ export class ToolExecutor {
|
||||
* Handle complete block execution.
|
||||
*
|
||||
* This is the main execution flow for a tool:
|
||||
* 1. Run PreToolUse hooks (if enabled) - can block execution
|
||||
* 2. Execute the actual tool
|
||||
* 3. Run PostToolUse hooks (if enabled) - cannot block, only observe
|
||||
* 4. Add hook context modifications to the conversation
|
||||
* 5. Update focus chain tracking
|
||||
* 1. Execute the actual tool (tool handlers now run PreToolUse hooks post-approval)
|
||||
* 2. Run PostToolUse hooks (if enabled) - cannot block, only observe
|
||||
* 3. Add hook context modifications to the conversation
|
||||
* 4. Update focus chain tracking
|
||||
*
|
||||
* Note: PreToolUse hooks are now executed by individual tool handlers after approval
|
||||
* and before the actual tool operation. This provides better UX as approval dialogs
|
||||
* appear immediately without hook execution delay.
|
||||
*
|
||||
* Hooks are executed with streaming output to provide real-time feedback.
|
||||
* PreToolUse hooks can prevent tool execution by returning shouldContinue: false.
|
||||
* PostToolUse hooks are for observation/logging only and cannot block.
|
||||
*
|
||||
* @param block The complete tool use block with all parameters
|
||||
@@ -553,96 +557,6 @@ export class ToolExecutor {
|
||||
// Track if we need to cancel after hooks complete
|
||||
let shouldCancelAfterHook = false
|
||||
|
||||
// ============================================================
|
||||
// PHASE 1: Run PreToolUse hook (OUTSIDE try-catch-finally)
|
||||
// This allows early return on cancellation without triggering finally block
|
||||
// ============================================================
|
||||
if (hooksEnabled) {
|
||||
const { executeHook } = await import("../hooks/hook-executor")
|
||||
|
||||
// Build pending tool info for display
|
||||
const pendingToolInfo: any = {
|
||||
tool: block.name,
|
||||
}
|
||||
|
||||
// Add relevant parameters for display based on tool type
|
||||
if (block.params.path) {
|
||||
pendingToolInfo.path = block.params.path
|
||||
}
|
||||
if (block.params.command) {
|
||||
pendingToolInfo.command = block.params.command
|
||||
}
|
||||
if (block.params.content && typeof block.params.content === "string") {
|
||||
pendingToolInfo.content = block.params.content.slice(0, 200)
|
||||
}
|
||||
if (block.params.diff && typeof block.params.diff === "string") {
|
||||
pendingToolInfo.diff = block.params.diff.slice(0, 200)
|
||||
}
|
||||
if (block.params.regex) {
|
||||
pendingToolInfo.regex = block.params.regex
|
||||
}
|
||||
if (block.params.url) {
|
||||
pendingToolInfo.url = block.params.url
|
||||
}
|
||||
// For MCP operations, show tool/resource identifiers
|
||||
if (block.params.tool_name) {
|
||||
pendingToolInfo.mcpTool = block.params.tool_name
|
||||
}
|
||||
if (block.params.server_name) {
|
||||
pendingToolInfo.mcpServer = block.params.server_name
|
||||
}
|
||||
if (block.params.uri) {
|
||||
pendingToolInfo.resourceUri = block.params.uri
|
||||
}
|
||||
|
||||
const preToolResult = await executeHook({
|
||||
hookName: "PreToolUse",
|
||||
hookInput: {
|
||||
preToolUse: {
|
||||
toolName: block.name,
|
||||
parameters: block.params,
|
||||
},
|
||||
},
|
||||
isCancellable: true,
|
||||
say: this.say,
|
||||
setActiveHookExecution: this.setActiveHookExecution,
|
||||
clearActiveHookExecution: this.clearActiveHookExecution,
|
||||
messageStateHandler: this.messageStateHandler,
|
||||
taskId: this.taskId,
|
||||
hooksEnabled,
|
||||
toolName: block.name,
|
||||
pendingToolInfo,
|
||||
})
|
||||
|
||||
// Handle cancellation from hook
|
||||
if (preToolResult.cancel === true) {
|
||||
// Trigger task cancellation (same as clicking cancel button)
|
||||
await config.callbacks.cancelTask()
|
||||
// Early return - never enters try-catch-finally, so PostToolUse won't run
|
||||
return
|
||||
}
|
||||
|
||||
// If task was aborted (e.g., via cancel button during hook), stop execution
|
||||
if (this.taskState.abort) {
|
||||
shouldCancelAfterHook = true
|
||||
}
|
||||
|
||||
// Add context modification to the conversation if provided by the hook
|
||||
if (preToolResult.contextModification) {
|
||||
this.addHookContextToConversation(preToolResult.contextModification, "PreToolUse")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PHASE 2: Execute tool with PostToolUse hook in finally block
|
||||
// This only runs if PreToolUse didn't cancel above
|
||||
// ============================================================
|
||||
|
||||
// Check abort again before tool execution (could have been set by PreToolUse hook)
|
||||
if (this.taskState.abort) {
|
||||
return
|
||||
}
|
||||
|
||||
let executionSuccess = true
|
||||
let toolResult: any = null
|
||||
let toolWasExecuted = false
|
||||
|
||||
@@ -126,6 +126,18 @@ export class AccessMcpResourceHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
await config.callbacks.say("mcp_server_request_started")
|
||||
|
||||
// Execute the MCP resource access
|
||||
|
||||
@@ -269,6 +269,19 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
this.appliedCommit = commit
|
||||
this.config = config
|
||||
|
||||
// Run PreToolUse hook before applying changes
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
await provider.reset()
|
||||
return "The user denied this patch operation."
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Apply the commit
|
||||
const applyResults = await this.applyCommit(commit)
|
||||
|
||||
|
||||
@@ -52,6 +52,18 @@ export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHand
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
// Run PreToolUse hook before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Show notification if enabled
|
||||
if (config.autoApprovalSettings.enableNotifications) {
|
||||
showSystemNotification({
|
||||
|
||||
@@ -105,6 +105,18 @@ export class BrowserToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Start loading spinner
|
||||
await config.callbacks.say("browser_action_result", "")
|
||||
|
||||
|
||||
@@ -205,6 +205,18 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Setup timeout notification for long-running auto-approved commands
|
||||
let timeoutId: NodeJS.Timeout | undefined
|
||||
if (didAutoApprove && config.autoApprovalSettings.enableNotifications) {
|
||||
|
||||
@@ -134,6 +134,18 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,13 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
resolutionMethod: (typeof pathResult !== "string" ? "hint" : "primary_fallback") as "hint" | "primary_fallback",
|
||||
}
|
||||
|
||||
// Check clineignore access
|
||||
const accessValidation = this.validator.checkClineIgnorePath(relDirPath!)
|
||||
if (!accessValidation.ok) {
|
||||
await config.callbacks.say("clineignore_error", relDirPath)
|
||||
return formatResponse.toolError(formatResponse.clineIgnoreError(relDirPath!))
|
||||
}
|
||||
|
||||
// Execute the actual list files operation
|
||||
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
|
||||
|
||||
@@ -151,6 +158,18 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,18 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Execute the actual file read operation
|
||||
const supportsImages = config.api.getModel().info.supportsImages ?? false
|
||||
const fileContent = await extractFileContent(absolutePath, supportsImages)
|
||||
|
||||
@@ -357,6 +357,18 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,18 @@ export class UseMcpToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Show MCP request started message
|
||||
await config.callbacks.say("mcp_server_request_started")
|
||||
|
||||
|
||||
@@ -109,6 +109,18 @@ export class WebFetchToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Execute the actual fetch
|
||||
const urlContentFetcher = config.services?.urlContentFetcher as UrlContentFetcher
|
||||
|
||||
|
||||
@@ -270,6 +270,20 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
await config.services.diffViewProvider.revertChanges()
|
||||
await config.services.diffViewProvider.reset()
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Mark the file as edited by Cline
|
||||
config.services.fileContextTracker.markFileAsEditedByCline(relPath)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { StateManager } from "../../../storage/StateManager"
|
||||
import type { MessageStateHandler } from "../../message-state"
|
||||
import type { TaskState } from "../../TaskState"
|
||||
import type { AutoApprove } from "../../tools/autoApprove"
|
||||
import type { HookExecution } from "../../types/HookExecution"
|
||||
import type { ToolExecutorCoordinator } from "../ToolExecutorCoordinator"
|
||||
import { TASK_CALLBACKS_KEYS, TASK_CONFIG_KEYS, TASK_SERVICES_KEYS } from "../utils/ToolConstants"
|
||||
|
||||
@@ -116,6 +117,11 @@ export interface TaskCallbacks {
|
||||
applyLatestBrowserSettings: () => Promise<BrowserSession>
|
||||
|
||||
switchToActMode: () => Promise<boolean>
|
||||
|
||||
// Hook execution callbacks
|
||||
setActiveHookExecution: (hookExecution: HookExecution) => Promise<void>
|
||||
clearActiveHookExecution: () => Promise<void>
|
||||
getActiveHookExecution: () => Promise<HookExecution | undefined>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,6 +64,9 @@ export const TASK_CALLBACKS_KEYS = [
|
||||
"cancelTask",
|
||||
"updateTaskHistory",
|
||||
"switchToActMode",
|
||||
"setActiveHookExecution",
|
||||
"clearActiveHookExecution",
|
||||
"getActiveHookExecution",
|
||||
] as const
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { PreToolUseHookCancellationError } from "@core/hooks/PreToolUseHookCancellationError"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
|
||||
/**
|
||||
* Utility functions for tool hook execution.
|
||||
*/
|
||||
export class ToolHookUtils {
|
||||
/**
|
||||
* Runs the PreToolUse hook if enabled.
|
||||
*
|
||||
* This should be called by tool handlers after approval succeeds
|
||||
* but before the actual tool execution begins.
|
||||
*
|
||||
* @param config The task configuration
|
||||
* @param block The tool use block being executed
|
||||
* @returns Promise<boolean> - true if execution should continue, false if hook cancelled
|
||||
* @throws PreToolUseHookCancellationError if the hook requests cancellation
|
||||
*/
|
||||
static async runPreToolUseIfEnabled(config: TaskConfig, block: ToolUse): Promise<boolean> {
|
||||
// Check if hooks are enabled via user setting
|
||||
const hooksEnabled = config.services.stateManager.getGlobalSettingsKey("hooksEnabled")
|
||||
|
||||
if (!hooksEnabled) {
|
||||
return true // Hooks disabled, continue execution
|
||||
}
|
||||
|
||||
// Import the hook executor dynamically
|
||||
const { executeHook } = await import("@core/hooks/hook-executor")
|
||||
|
||||
// Build pending tool info for display
|
||||
const pendingToolInfo: any = {
|
||||
tool: block.name,
|
||||
}
|
||||
|
||||
// Add relevant parameters for display based on tool type
|
||||
if (block.params.path) {
|
||||
pendingToolInfo.path = block.params.path
|
||||
}
|
||||
if (block.params.command) {
|
||||
pendingToolInfo.command = block.params.command
|
||||
}
|
||||
if (block.params.content && typeof block.params.content === "string") {
|
||||
pendingToolInfo.content = block.params.content.slice(0, 200)
|
||||
}
|
||||
if (block.params.diff && typeof block.params.diff === "string") {
|
||||
pendingToolInfo.diff = block.params.diff.slice(0, 200)
|
||||
}
|
||||
if (block.params.regex) {
|
||||
pendingToolInfo.regex = block.params.regex
|
||||
}
|
||||
if (block.params.url) {
|
||||
pendingToolInfo.url = block.params.url
|
||||
}
|
||||
// For MCP operations, show tool/resource identifiers
|
||||
if (block.params.tool_name) {
|
||||
pendingToolInfo.mcpTool = block.params.tool_name
|
||||
}
|
||||
if (block.params.server_name) {
|
||||
pendingToolInfo.mcpServer = block.params.server_name
|
||||
}
|
||||
if (block.params.uri) {
|
||||
pendingToolInfo.resourceUri = block.params.uri
|
||||
}
|
||||
|
||||
// Execute the PreToolUse hook
|
||||
const preToolResult = await executeHook({
|
||||
hookName: "PreToolUse",
|
||||
hookInput: {
|
||||
preToolUse: {
|
||||
toolName: block.name,
|
||||
parameters: block.params,
|
||||
},
|
||||
},
|
||||
isCancellable: true,
|
||||
say: config.callbacks.say,
|
||||
setActiveHookExecution: config.callbacks.setActiveHookExecution,
|
||||
clearActiveHookExecution: config.callbacks.clearActiveHookExecution,
|
||||
messageStateHandler: config.messageState,
|
||||
taskId: config.taskId,
|
||||
hooksEnabled,
|
||||
toolName: block.name,
|
||||
pendingToolInfo,
|
||||
})
|
||||
|
||||
// Handle cancellation from hook
|
||||
if (preToolResult.cancel === true) {
|
||||
throw new PreToolUseHookCancellationError(preToolResult.errorMessage || "PreToolUse hook requested cancellation")
|
||||
}
|
||||
|
||||
// If task was aborted (e.g., via cancel button during hook), throw cancellation error
|
||||
if (config.taskState.abort) {
|
||||
throw new PreToolUseHookCancellationError("Task was aborted during PreToolUse hook execution")
|
||||
}
|
||||
|
||||
// Add context modification to the conversation if provided by the hook
|
||||
if (preToolResult.contextModification) {
|
||||
ToolHookUtils.addHookContextToConversation(config, preToolResult.contextModification, "PreToolUse")
|
||||
}
|
||||
|
||||
return true // Hook succeeded, continue execution
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds hook context modification to the conversation if provided.
|
||||
* Parses the context to extract type prefix and formats as XML.
|
||||
*
|
||||
* @param config The task configuration
|
||||
* @param contextModification The context string from the hook output
|
||||
* @param source The hook source name ("PreToolUse" or "PostToolUse")
|
||||
*/
|
||||
private static addHookContextToConversation(
|
||||
config: TaskConfig,
|
||||
contextModification: string | undefined,
|
||||
source: string,
|
||||
): void {
|
||||
if (!contextModification) {
|
||||
return
|
||||
}
|
||||
|
||||
const contextText = contextModification.trim()
|
||||
if (!contextText) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract context type from first line if specified (e.g., "WORKSPACE_RULES: ...")
|
||||
const lines = contextText.split("\n")
|
||||
const firstLine = lines[0]
|
||||
let contextType = "general"
|
||||
let content = contextText
|
||||
|
||||
// Check if first line specifies a type: "TYPE: content"
|
||||
const typeMatchRegex = /^([A-Z_]+):\s*(.*)/
|
||||
const typeMatch = typeMatchRegex.exec(firstLine)
|
||||
if (typeMatch) {
|
||||
contextType = typeMatch[1].toLowerCase()
|
||||
const remainingLines = lines.slice(1).filter((l: string) => l.trim())
|
||||
content = typeMatch[2] ? [typeMatch[2], ...remainingLines].join("\n") : remainingLines.join("\n")
|
||||
}
|
||||
|
||||
const hookContextBlock = {
|
||||
type: "text" as const,
|
||||
text: `<hook_context source="${source}" type="${contextType}">\n${content}\n</hook_context>`,
|
||||
}
|
||||
|
||||
config.taskState.userMessageContent.push(hookContextBlock)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Represents an active hook execution that can be cancelled.
|
||||
* This is tracked in TaskState to allow cancellation via UI or programmatic triggers.
|
||||
*/
|
||||
export interface HookExecution {
|
||||
/** The name of the hook being executed (e.g., "PreToolUse", "PostToolUse") */
|
||||
hookName: string
|
||||
/** The name of the tool that triggered this hook (for PreToolUse/PostToolUse hooks) */
|
||||
toolName?: string
|
||||
/** The timestamp of the message showing hook execution status */
|
||||
messageTs: number
|
||||
/** The abort controller used to cancel the hook execution */
|
||||
abortController: AbortController
|
||||
}
|
||||
Reference in New Issue
Block a user