Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/__mocks__/vscode.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ const vscode = {
this.pattern = pattern
}
},
CodeActionKind: {
Empty: "",
QuickFix: "quickfix",
Refactor: "refactor",
RefactorExtract: "refactor.extract",
RefactorInline: "refactor.inline",
RefactorRewrite: "refactor.rewrite",
Source: "source",
SourceOrganizeImports: "source.organizeImports",
SourceFixAll: "source.fixAll",
Notebook: "notebook",
},
}

module.exports = vscode
1,948 changes: 189 additions & 1,759 deletions src/core/Cline.ts

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions src/core/tool-handlers/ToolUseHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// src/core/tool-handlers/ToolUseHandler.ts
import { ToolUse } from "../assistant-message"
import { Cline } from "../Cline"

export abstract class ToolUseHandler {
protected cline: Cline
protected toolUse: ToolUse

constructor(cline: Cline, toolUse: ToolUse) {
this.cline = cline
this.toolUse = toolUse
}

/**
* Handle the tool use, both partial and complete states
* @returns Promise<boolean> true if the tool was handled completely, false if only partially handled (streaming)
*/
abstract handle(): Promise<boolean>

/**
* Handle a partial tool use (streaming)
* This method should update the UI/state based on the partial data received so far.
* It typically returns void as the handling is ongoing.
*/
protected abstract handlePartial(): Promise<void>

/**
* Handle a complete tool use
* This method performs the final action for the tool use after all data is received.
* It typically returns void as the action is completed within this method.
*/
protected abstract handleComplete(): Promise<void>

/**
* Validate the tool parameters
* @throws Error if validation fails
*/
abstract validateParams(): void

/**
* Helper to remove potentially incomplete closing tags from parameters during streaming.
* Example: <path>src/my</path> might stream as "src/my</pat" initially.
* This helps get the usable value during partial updates.
*/
protected removeClosingTag(tag: string, text?: string): string {
// Only apply removal if it's a partial tool use
if (!this.toolUse.partial) {
return text || ""
}
if (!text) {
return ""
}
// Regex to match a potentially incomplete closing tag at the end of the string
// Example: Matches </tag>, </ta>, </t>, </
const tagRegex = new RegExp(
`\\s*<\\/?${tag
.split("")
.map((char) => `(?:${char})?`) // Match each character optionally
.join("")}$`,
"g",
)
return text.replace(tagRegex, "")
}

/**
* Helper to handle missing parameters consistently.
* Increments mistake count and formats a standard error message for the API.
*/
protected async handleMissingParam(paramName: string): Promise<string> {
this.cline.consecutiveMistakeCount++ // Assuming consecutiveMistakeCount is accessible or moved
// Consider making sayAndCreateMissingParamError public or moving it to a shared utility
// if consecutiveMistakeCount remains private and central to Cline.
// For now, assuming it can be called or its logic replicated here/in base class.
return await this.cline.sayAndCreateMissingParamError(
this.toolUse.name,
paramName,
this.toolUse.params.path, // Assuming path might be relevant context, though not always present
)
}
}
83 changes: 83 additions & 0 deletions src/core/tool-handlers/ToolUseHandlerFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// src/core/tool-handlers/ToolUseHandlerFactory.ts
import { ToolUse, ToolUseName } from "../assistant-message"
import { Cline } from "../Cline"
import { ToolUseHandler } from "./ToolUseHandler"
// Import statements for individual handlers (files will be created later)
import { WriteToFileHandler } from "./tools/WriteToFileHandler"
import { ReadFileHandler } from "./tools/ReadFileHandler"
import { ExecuteCommandHandler } from "./tools/ExecuteCommandHandler"
import { ApplyDiffHandler } from "./tools/ApplyDiffHandler"
import { SearchFilesHandler } from "./tools/SearchFilesHandler"
import { ListFilesHandler } from "./tools/ListFilesHandler"
import { ListCodeDefinitionNamesHandler } from "./tools/ListCodeDefinitionNamesHandler"
import { BrowserActionHandler } from "./tools/BrowserActionHandler"
import { UseMcpToolHandler } from "./tools/UseMcpToolHandler"
import { AccessMcpResourceHandler } from "./tools/AccessMcpResourceHandler"
import { AskFollowupQuestionHandler } from "./tools/AskFollowupQuestionHandler"
import { AttemptCompletionHandler } from "./tools/AttemptCompletionHandler"
import { SwitchModeHandler } from "./tools/SwitchModeHandler"
import { NewTaskHandler } from "./tools/NewTaskHandler"
import { FetchInstructionsHandler } from "./tools/FetchInstructionsHandler"
import { InsertContentHandler } from "./tools/InsertContentHandler"
import { SearchAndReplaceHandler } from "./tools/SearchAndReplaceHandler"
import { formatResponse } from "../prompts/responses" // Needed for error handling

export class ToolUseHandlerFactory {
static createHandler(cline: Cline, toolUse: ToolUse): ToolUseHandler | null {
try {
switch (toolUse.name) {
case "write_to_file":
return new WriteToFileHandler(cline, toolUse)
case "read_file":
return new ReadFileHandler(cline, toolUse)
case "execute_command":
return new ExecuteCommandHandler(cline, toolUse)
case "apply_diff":
return new ApplyDiffHandler(cline, toolUse)
case "search_files":
return new SearchFilesHandler(cline, toolUse)
case "list_files":
return new ListFilesHandler(cline, toolUse)
case "list_code_definition_names":
return new ListCodeDefinitionNamesHandler(cline, toolUse)
case "browser_action":
return new BrowserActionHandler(cline, toolUse)
case "use_mcp_tool":
return new UseMcpToolHandler(cline, toolUse)
case "access_mcp_resource":
return new AccessMcpResourceHandler(cline, toolUse)
case "ask_followup_question":
return new AskFollowupQuestionHandler(cline, toolUse)
case "attempt_completion":
return new AttemptCompletionHandler(cline, toolUse)
case "switch_mode":
return new SwitchModeHandler(cline, toolUse)
case "new_task":
return new NewTaskHandler(cline, toolUse)
case "fetch_instructions":
return new FetchInstructionsHandler(cline, toolUse)
case "insert_content":
return new InsertContentHandler(cline, toolUse)
case "search_and_replace":
return new SearchAndReplaceHandler(cline, toolUse)
default:
// Handle unknown tool names gracefully
console.error(`No handler found for tool: ${toolUse.name}`)
// It's important the main loop handles this null return
// by pushing an appropriate error message back to the API.
// We avoid throwing an error here to let the caller decide.
return null
}
} catch (error) {
// Catch potential errors during handler instantiation (though unlikely with current structure)
console.error(`Error creating handler for tool ${toolUse.name}:`, error)
// Push an error result back to the API via Cline instance
// Pass both the toolUse object and the error content
cline.pushToolResult(
toolUse,
formatResponse.toolError(`Error initializing handler for tool ${toolUse.name}.`),
)
return null // Indicate failure to create handler
}
}
}
126 changes: 126 additions & 0 deletions src/core/tool-handlers/tools/AccessMcpResourceHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { ToolUse } from "../../assistant-message" // Using generic ToolUse
import { Cline } from "../../Cline"
import { ToolUseHandler } from "../ToolUseHandler"
import { formatResponse } from "../../prompts/responses"
import { ClineAskUseMcpServer } from "../../../shared/ExtensionMessage"
import { telemetryService } from "../../../services/telemetry/TelemetryService"

export class AccessMcpResourceHandler extends ToolUseHandler {
// No specific toolUse type override needed

constructor(cline: Cline, toolUse: ToolUse) {
super(cline, toolUse)
}

async handle(): Promise<boolean> {
if (this.toolUse.partial) {
await this.handlePartial()
return false // Indicate partial handling
} else {
await this.handleComplete()
return true // Indicate complete handling
}
}

validateParams(): void {
if (!this.toolUse.params.server_name) {
throw new Error("Missing required parameter 'server_name'")
}
if (!this.toolUse.params.uri) {
throw new Error("Missing required parameter 'uri'")
}
}

protected async handlePartial(): Promise<void> {
const serverName = this.toolUse.params.server_name
const uri = this.toolUse.params.uri
if (!serverName || !uri) return // Need server and uri for message

const partialMessage = JSON.stringify({
type: "access_mcp_resource",
serverName: this.removeClosingTag("server_name", serverName),
uri: this.removeClosingTag("uri", uri),
} satisfies ClineAskUseMcpServer)

try {
await this.cline.ask("use_mcp_server", partialMessage, true)
} catch (error) {
console.warn("AccessMcpResourceHandler: ask for partial update interrupted.", error)
}
}

protected async handleComplete(): Promise<void> {
const serverName = this.toolUse.params.server_name
const uri = this.toolUse.params.uri

// --- Parameter Validation ---
if (!serverName) {
this.cline.consecutiveMistakeCount++
await this.cline.pushToolResult(
this.toolUse,
await this.cline.sayAndCreateMissingParamError("access_mcp_resource", "server_name"),
)
return
}
if (!uri) {
this.cline.consecutiveMistakeCount++
await this.cline.pushToolResult(
this.toolUse,
await this.cline.sayAndCreateMissingParamError("access_mcp_resource", "uri"),
)
return
}

// --- Access MCP Resource ---
try {
this.cline.consecutiveMistakeCount = 0 // Reset on successful validation

// --- Ask for Approval ---
const completeMessage = JSON.stringify({
type: "access_mcp_resource",
serverName: serverName,
uri: uri,
} satisfies ClineAskUseMcpServer)

const didApprove = await this.cline.askApprovalHelper(this.toolUse, "use_mcp_server", completeMessage)
if (!didApprove) {
// pushToolResult handled by helper
return
}

// --- Call MCP Hub ---
await this.cline.say("mcp_server_request_started") // Show loading/request state
const mcpHub = this.cline.providerRef.deref()?.getMcpHub()
if (!mcpHub) {
throw new Error("MCP Hub is not available.")
}

const resourceResult = await mcpHub.readResource(serverName, uri)

// --- Process Result ---
const resourceResultPretty =
resourceResult?.contents
?.map((item) => item.text) // Extract only text content for the main result
.filter(Boolean)
.join("\n\n") || "(Empty response)"

// Extract images separately
const images: string[] = []
resourceResult?.contents?.forEach((item) => {
if (item.mimeType?.startsWith("image") && item.blob) {
images.push(item.blob) // Assuming blob is base64 data URL
}
})

await this.cline.say("mcp_server_response", resourceResultPretty, images.length > 0 ? images : undefined) // Show result text and images
await this.cline.pushToolResult(
this.toolUse,
formatResponse.toolResult(resourceResultPretty, images.length > 0 ? images : undefined),
)
telemetryService.captureToolUsage(this.cline.taskId, this.toolUse.name)
} catch (error: any) {
// Handle errors during approval or MCP call
await this.cline.handleErrorHelper(this.toolUse, "accessing MCP resource", error)
}
}
}
Loading