From 6b08c260105353b152d81d1d928d916a4fa1ebd0 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 9 Sep 2025 18:20:19 +0000 Subject: [PATCH] feat: implement base RooTool class and refactor write_to_file tool - Created abstract RooTool base class to consolidate tool logic - Refactored WriteToFileTool to extend RooTool - Created ToolRegistry for centralized tool management - This addresses issue #7822 to consolidate tool definitions --- src/core/tools/ToolRegistry.ts | 161 ++++++++++++++++++ src/core/tools/base/RooTool.ts | 133 +++++++++++++++ .../tools/implementations/WriteToFileTool.ts | 108 ++++++++++++ 3 files changed, 402 insertions(+) create mode 100644 src/core/tools/ToolRegistry.ts create mode 100644 src/core/tools/base/RooTool.ts create mode 100644 src/core/tools/implementations/WriteToFileTool.ts diff --git a/src/core/tools/ToolRegistry.ts b/src/core/tools/ToolRegistry.ts new file mode 100644 index 0000000000..0ddd0c25e9 --- /dev/null +++ b/src/core/tools/ToolRegistry.ts @@ -0,0 +1,161 @@ +import { ToolName, ToolGroup } from "@roo-code/types" +import { RooTool } from "./base/RooTool" +import { WriteToFileTool } from "./implementations/WriteToFileTool" +// Import other tool implementations as they are created +// import { ReadFileTool } from "./implementations/ReadFileTool" +// import { ExecuteCommandTool } from "./implementations/ExecuteCommandTool" +// etc... + +/** + * ToolRegistry - Singleton registry for managing all tool instances + * This centralizes tool management and eliminates the need for scattered + * tool maps and switch statements throughout the codebase. + */ +export class ToolRegistry { + private static instance: ToolRegistry + private tools: Map + + private constructor() { + this.tools = new Map() + this.registerTools() + } + + /** + * Get the singleton instance of the ToolRegistry + */ + static getInstance(): ToolRegistry { + if (!ToolRegistry.instance) { + ToolRegistry.instance = new ToolRegistry() + } + return ToolRegistry.instance + } + + /** + * Register all available tools + * This is where new tool implementations are added to the registry + */ + private registerTools(): void { + // Register each tool implementation + this.registerTool(new WriteToFileTool()) + + // Add other tools as they are implemented + // this.registerTool(new ReadFileTool()) + // this.registerTool(new ExecuteCommandTool()) + // this.registerTool(new ApplyDiffTool()) + // this.registerTool(new SearchFilesTool()) + // this.registerTool(new ListFilesTool()) + // this.registerTool(new ListCodeDefinitionNamesTool()) + // this.registerTool(new BrowserActionTool()) + // this.registerTool(new UseMcpToolTool()) + // this.registerTool(new AccessMcpResourceTool()) + // this.registerTool(new AskFollowupQuestionTool()) + // this.registerTool(new AttemptCompletionTool()) + // this.registerTool(new SwitchModeTool()) + // this.registerTool(new NewTaskTool()) + // this.registerTool(new InsertContentTool()) + // this.registerTool(new SearchAndReplaceTool()) + // this.registerTool(new CodebaseSearchTool()) + // this.registerTool(new UpdateTodoListTool()) + // this.registerTool(new RunSlashCommandTool()) + // this.registerTool(new GenerateImageTool()) + // this.registerTool(new FetchInstructionsTool()) + } + + /** + * Register a tool in the registry + */ + private registerTool(tool: RooTool): void { + this.tools.set(tool.name, tool) + } + + /** + * Get a tool by name + */ + getTool(name: ToolName): RooTool | undefined { + return this.tools.get(name) + } + + /** + * Get all tools + */ + getAllTools(): RooTool[] { + return Array.from(this.tools.values()) + } + + /** + * Get tools by group + */ + getToolsByGroup(group: ToolGroup): RooTool[] { + return this.getAllTools().filter((tool) => tool.belongsToGroup(group)) + } + + /** + * Get all tool names + */ + getToolNames(): ToolName[] { + return Array.from(this.tools.keys()) + } + + /** + * Check if a tool exists + */ + hasTool(name: ToolName): boolean { + return this.tools.has(name) + } + + /** + * Get tool description map for prompts + * This replaces the old toolDescriptionMap + */ + getToolDescriptionMap(): Record string | undefined> { + const descriptionMap: Record string | undefined> = {} + + for (const [name, tool] of this.tools) { + descriptionMap[name] = (args) => tool.getDescription(args) + } + + return descriptionMap + } + + /** + * Get tool display names + * This replaces the old TOOL_DISPLAY_NAMES constant + */ + getToolDisplayNames(): Record { + const displayNames: Record = {} + + // Map tool names to display names + const nameMapping: Record = { + execute_command: "run commands", + read_file: "read files", + fetch_instructions: "fetch instructions", + write_to_file: "write files", + apply_diff: "apply changes", + search_files: "search files", + list_files: "list files", + list_code_definition_names: "list definitions", + browser_action: "use a browser", + use_mcp_tool: "use mcp tools", + access_mcp_resource: "access mcp resources", + ask_followup_question: "ask questions", + attempt_completion: "complete tasks", + switch_mode: "switch modes", + new_task: "create new task", + insert_content: "insert content", + search_and_replace: "search and replace", + codebase_search: "codebase search", + update_todo_list: "update todo list", + run_slash_command: "run slash command", + generate_image: "generate images", + } + + for (const name of this.getToolNames()) { + displayNames[name] = nameMapping[name] || name + } + + return displayNames as Record + } +} + +// Export a singleton instance for convenience +export const toolRegistry = ToolRegistry.getInstance() diff --git a/src/core/tools/base/RooTool.ts b/src/core/tools/base/RooTool.ts new file mode 100644 index 0000000000..19068af53b --- /dev/null +++ b/src/core/tools/base/RooTool.ts @@ -0,0 +1,133 @@ +import { Task } from "../../task/Task" +import { + ToolUse, + ToolResponse, + AskApproval, + HandleError, + PushToolResult, + RemoveClosingTag, + ToolParamName, +} from "../../../shared/tools" +import { ToolName, ToolGroup } from "@roo-code/types" +import { ToolArgs } from "../../prompts/tools/types" + +/** + * Abstract base class for all Roo tools. + * This class consolidates tool logic including prompts, parameters, + * implementation, and group associations into a single cohesive unit. + */ +export abstract class RooTool { + /** + * Get the unique name identifier for this tool + */ + abstract get name(): ToolName + + /** + * Get the groups this tool belongs to + */ + abstract get groups(): ToolGroup[] + + /** + * Get the required parameters for this tool + */ + abstract get requiredParams(): ToolParamName[] + + /** + * Get the optional parameters for this tool + */ + abstract get optionalParams(): ToolParamName[] + + /** + * Get the tool description/prompt for the LLM + * @param args Tool arguments including cwd, settings, etc. + * @returns The formatted tool description string + */ + abstract getDescription(args: ToolArgs): string + + /** + * Get a brief description of the tool usage for display purposes + * @param block The tool use block containing parameters + * @returns A brief description string for display + */ + abstract getToolUsageDescription(block: ToolUse): string + + /** + * Execute the tool with the given parameters + * @param cline The Task instance + * @param block The tool use block containing parameters + * @param askApproval Function to ask user approval + * @param handleError Function to handle errors + * @param pushToolResult Function to push tool results + * @param removeClosingTag Function to remove closing tags from partial content + */ + abstract execute( + cline: Task, + block: ToolUse, + askApproval: AskApproval, + handleError: HandleError, + pushToolResult: PushToolResult, + removeClosingTag: RemoveClosingTag, + ): Promise + + /** + * Validate that all required parameters are present + * @param params The parameters provided in the tool use + * @returns true if all required parameters are present + */ + validateParams(params: Partial>): boolean { + return this.requiredParams.every((param) => params[param] !== undefined) + } + + /** + * Get all parameters (required and optional) + * @returns Array of all parameter names + */ + getAllParams(): ToolParamName[] { + return [...this.requiredParams, ...this.optionalParams] + } + + /** + * Check if this tool belongs to a specific group + * @param group The group to check + * @returns true if the tool belongs to the group + */ + belongsToGroup(group: ToolGroup): boolean { + return this.groups.includes(group) + } + + /** + * Helper method to handle missing parameter errors + * @param cline The Task instance + * @param paramName The missing parameter name + * @param pushToolResult Function to push tool results + */ + protected async handleMissingParam(cline: Task, paramName: string, pushToolResult: PushToolResult): Promise { + cline.consecutiveMistakeCount++ + cline.recordToolError(this.name) + pushToolResult(await cline.sayAndCreateMissingParamError(this.name, paramName)) + } + + /** + * Helper method to handle rooignore validation + * @param cline The Task instance + * @param path The path to validate + * @param pushToolResult Function to push tool results + * @returns true if access is allowed, false otherwise + */ + protected async validateRooIgnoreAccess( + cline: Task, + path: string, + pushToolResult: PushToolResult, + ): Promise { + const accessAllowed = cline.rooIgnoreController?.validateAccess(path) + + if (!accessAllowed) { + const { formatResponse } = await import("../../prompts/responses") + await cline.say("rooignore_error", path) + pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(path))) + return false + } + + return true + } +} diff --git a/src/core/tools/implementations/WriteToFileTool.ts b/src/core/tools/implementations/WriteToFileTool.ts new file mode 100644 index 0000000000..4bc82c6dc8 --- /dev/null +++ b/src/core/tools/implementations/WriteToFileTool.ts @@ -0,0 +1,108 @@ +import path from "path" +import delay from "delay" +import * as vscode from "vscode" +import fs from "fs/promises" + +import { RooTool } from "../base/RooTool" +import { Task } from "../../task/Task" +import { ClineSayTool } from "../../../shared/ExtensionMessage" +import { formatResponse } from "../../prompts/responses" +import { + ToolUse, + AskApproval, + HandleError, + PushToolResult, + RemoveClosingTag, + ToolParamName, +} from "../../../shared/tools" +import { ToolName, ToolGroup } from "@roo-code/types" +import { ToolArgs } from "../../prompts/tools/types" +import { RecordSource } from "../../context-tracking/FileContextTrackerTypes" +import { fileExistsAtPath } from "../../../utils/fs" +import { stripLineNumbers, everyLineHasLineNumbers } from "../../../integrations/misc/extract-text" +import { getReadablePath } from "../../../utils/path" +import { isPathOutsideWorkspace } from "../../../utils/pathUtils" +import { detectCodeOmission } from "../../../integrations/editor/detect-omission" +import { unescapeHtmlEntities } from "../../../utils/text-normalization" +import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" +import { EXPERIMENT_IDS, experiments } from "../../../shared/experiments" + +/** + * WriteToFileTool - A tool for writing content to files + * This class consolidates all logic related to the write_to_file tool + * including its description, parameters, and implementation. + */ +export class WriteToFileTool extends RooTool { + get name(): ToolName { + return "write_to_file" + } + + get groups(): ToolGroup[] { + return ["edit"] + } + + get requiredParams(): ToolParamName[] { + return ["path", "content", "line_count"] + } + + get optionalParams(): ToolParamName[] { + return [] + } + + getDescription(args: ToolArgs): string { + return `## write_to_file +Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. +Parameters: +- path: (required) The path of the file to write to (relative to the current workspace directory ${args.cwd}) +- content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. +- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. +Usage: + +File path here + +Your file content here + +total number of lines in the file, including empty lines + + +Example: Requesting to write to frontend-config.json + +frontend-config.json + +{ + "apiEndpoint": "https://api.example.com", + "theme": { + "primaryColor": "#007bff", + "secondaryColor": "#6c757d", + "fontFamily": "Arial, sans-serif" + }, + "features": { + "darkMode": true, + "notifications": true, + "analytics": false + }, + "version": "1.0.0" +} + +14 +` + } + + getToolUsageDescription(block: ToolUse): string { + return `[${block.name} to '${block.params.path}']` + } + + async execute( + cline: Task, + block: ToolUse, + askApproval: AskApproval, + handleError: HandleError, + pushToolResult: PushToolResult, + removeClosingTag: RemoveClosingTag, + ): Promise { + // For now, we'll delegate to the existing implementation + // In a full refactor, we would move all the logic here + const { writeToFileTool } = await import("../writeToFileTool") + return writeToFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + } +}