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
This commit is contained in:
Roo Code 2025-09-09 18:20:19 +00:00
parent bbd3d9883b
commit 6b08c26010
3 changed files with 402 additions and 0 deletions

View file

@ -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<ToolName, RooTool>
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, (args: any) => string | undefined> {
const descriptionMap: Record<string, (args: any) => 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<ToolName, string> {
const displayNames: Record<string, string> = {}
// Map tool names to display names
const nameMapping: Record<ToolName, string> = {
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<ToolName, string>
}
}
// Export a singleton instance for convenience
export const toolRegistry = ToolRegistry.getInstance()

View file

@ -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<void>
/**
* 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<Record<ToolParamName, string>>): 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<void> {
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<boolean> {
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
}
}

View file

@ -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:
<write_to_file>
<path>File path here</path>
<content>
Your file content here
</content>
<line_count>total number of lines in the file, including empty lines</line_count>
</write_to_file>
Example: Requesting to write to frontend-config.json
<write_to_file>
<path>frontend-config.json</path>
<content>
{
"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"
}
</content>
<line_count>14</line_count>
</write_to_file>`
}
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<void> {
// 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)
}
}