From 46e979c2899fef52e0efd1ebe5708d4c2e3609ac Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 19 Aug 2025 07:19:31 +0000 Subject: [PATCH] feat: add optional Morph Fast Apply tool for improved diff accuracy - Add morph_fast_apply tool that uses Morph API for 98% diff accuracy - Tool is conditionally available when morphApiKey is configured - Add morphApiKey to provider settings for API authentication - Integrate tool into presentAssistantMessage and system prompt - Tool provides fallback to standard apply_diff when API key not set Fixes #7206 --- packages/types/src/provider-settings.ts | 3 + packages/types/src/tool.ts | 1 + .../presentAssistantMessage.ts | 7 + src/core/prompts/tools/index.ts | 2 + src/core/prompts/tools/morph-fast-apply.ts | 51 +++++ src/core/tools/morphFastApplyTool.ts | 207 ++++++++++++++++++ src/shared/tools.ts | 3 +- 7 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 src/core/prompts/tools/morph-fast-apply.ts create mode 100644 src/core/tools/morphFastApplyTool.ts diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index fef7d811a4..317dbdc0ca 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -91,6 +91,9 @@ const baseProviderSettingsSchema = z.object({ // Model verbosity. verbosity: verbosityLevelsSchema.optional(), + + // Morph Fast Apply API key + morphApiKey: z.string().optional(), }) // Several of the providers share common model config properties. diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index 7a3fd21199..0ae818efc0 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -19,6 +19,7 @@ export const toolNames = [ "read_file", "write_to_file", "apply_diff", + "morph_fast_apply", "insert_content", "search_and_replace", "search_files", diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index acdc7f5412..9ceb8bdf69 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -12,6 +12,7 @@ import { listFilesTool } from "../tools/listFilesTool" import { getReadFileToolDescription, readFileTool } from "../tools/readFileTool" import { writeToFileTool } from "../tools/writeToFileTool" import { applyDiffTool } from "../tools/multiApplyDiffTool" +import { morphFastApplyTool } from "../tools/morphFastApplyTool" import { insertContentTool } from "../tools/insertContentTool" import { searchAndReplaceTool } from "../tools/searchAndReplaceTool" import { listCodeDefinitionNamesTool } from "../tools/listCodeDefinitionNamesTool" @@ -179,6 +180,8 @@ export async function presentAssistantMessage(cline: Task) { } } return `[${block.name}]` + case "morph_fast_apply": + return `[${block.name} for '${block.params.path}']` case "search_files": return `[${block.name} for '${block.params.regex}'${ block.params.file_pattern ? ` in '${block.params.file_pattern}'` : "" @@ -445,6 +448,10 @@ export async function presentAssistantMessage(cline: Task) { } break } + case "morph_fast_apply": + await checkpointSaveAndMark(cline) + await morphFastApplyTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + break case "insert_content": await checkpointSaveAndMark(cline) await insertContentTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index d455abb8d7..cd1a14a2f9 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -23,6 +23,7 @@ import { getSwitchModeDescription } from "./switch-mode" import { getNewTaskDescription } from "./new-task" import { getCodebaseSearchDescription } from "./codebase-search" import { getUpdateTodoListDescription } from "./update-todo-list" +import { getMorphFastApplyDescription } from "./morph-fast-apply" import { CodeIndexManager } from "../../../services/code-index/manager" // Map of tool names to their description functions @@ -46,6 +47,7 @@ const toolDescriptionMap: Record string | undefined> search_and_replace: (args) => getSearchAndReplaceDescription(args), apply_diff: (args) => args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "", + morph_fast_apply: (args) => getMorphFastApplyDescription(args), update_todo_list: (args) => getUpdateTodoListDescription(args), } diff --git a/src/core/prompts/tools/morph-fast-apply.ts b/src/core/prompts/tools/morph-fast-apply.ts new file mode 100644 index 0000000000..deade7de18 --- /dev/null +++ b/src/core/prompts/tools/morph-fast-apply.ts @@ -0,0 +1,51 @@ +import { ToolArgs } from "./types" + +export function getMorphFastApplyDescription(args: ToolArgs): string | undefined { + // Only show this tool if Morph API key is configured + if (!args.settings?.morphApiKey) { + return undefined + } + + return `## morph_fast_apply +Description: Use Morph Fast Apply to apply diffs with 98% accuracy. This tool uses Morph's advanced AI model to intelligently apply changes to files, handling complex edits that might fail with standard diff application. Only available when Morph API key is configured. + +Parameters: +- path: (required) The path of the file to modify (relative to the current workspace directory ${args.cwd}) +- diff: (required) The diff content in SEARCH/REPLACE format + +Diff format: +\`\`\` +<<<<<<< SEARCH +[exact content to find including whitespace] +======= +[new content to replace with] +>>>>>>> REPLACE +\`\`\` + +Usage: + +File path here + +Your search/replace content here + + + +Example: + +src/utils.ts + +<<<<<<< SEARCH +function calculateTotal(items) { + total = 0 + for item in items: + total += item + return total +======= +function calculateTotal(items) { + return items.reduce((sum, item) => sum + item, 0) +>>>>>>> REPLACE + + + +Note: This tool provides higher accuracy than standard apply_diff, especially for complex code transformations and when dealing with formatting variations.` +} diff --git a/src/core/tools/morphFastApplyTool.ts b/src/core/tools/morphFastApplyTool.ts new file mode 100644 index 0000000000..d953aec2c1 --- /dev/null +++ b/src/core/tools/morphFastApplyTool.ts @@ -0,0 +1,207 @@ +import path from "path" +import fs from "fs/promises" + +import { TelemetryService } from "@roo-code/telemetry" + +import { ClineSayTool } from "../../shared/ExtensionMessage" +import { getReadablePath } from "../../utils/path" +import { Task } from "../task/Task" +import { ToolUse, RemoveClosingTag, AskApproval, HandleError, PushToolResult } from "../../shared/tools" +import { formatResponse } from "../prompts/responses" +import { fileExistsAtPath } from "../../utils/fs" +import { RecordSource } from "../context-tracking/FileContextTrackerTypes" + +interface MorphApiResponse { + success: boolean + content?: string + error?: string +} + +/** + * Calls the Morph Fast Apply API to apply diffs with high accuracy + * @param apiKey The Morph API key + * @param originalContent The original file content + * @param diffContent The diff content to apply + * @returns The result from the Morph API + */ +async function callMorphApi(apiKey: string, originalContent: string, diffContent: string): Promise { + try { + // Using native fetch API (available in Node.js 18+) + const response = await fetch("https://api.morph.so/v1/fast-apply", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + original: originalContent, + diff: diffContent, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `Morph API error (${response.status}): ${errorText}`, + } + } + + const data = (await response.json()) as any + return { + success: true, + content: data.result || data.content, + } + } catch (error: any) { + return { + success: false, + error: `Failed to call Morph API: ${error.message}`, + } + } +} + +export async function morphFastApplyTool( + cline: Task, + block: ToolUse, + askApproval: AskApproval, + handleError: HandleError, + pushToolResult: PushToolResult, + removeClosingTag: RemoveClosingTag, +) { + const relPath: string | undefined = block.params.path + let diffContent: string | undefined = block.params.diff + + const sharedMessageProps: ClineSayTool = { + tool: "appliedDiff", + path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)), + diff: diffContent, + } + + try { + if (block.partial) { + // Update GUI message for partial blocks + await cline.ask("tool", JSON.stringify(sharedMessageProps), block.partial).catch(() => {}) + return + } + + if (!relPath) { + cline.consecutiveMistakeCount++ + cline.recordToolError("morph_fast_apply") + pushToolResult(await cline.sayAndCreateMissingParamError("morph_fast_apply", "path")) + return + } + + if (!diffContent) { + cline.consecutiveMistakeCount++ + cline.recordToolError("morph_fast_apply") + pushToolResult(await cline.sayAndCreateMissingParamError("morph_fast_apply", "diff")) + return + } + + // Check if file access is allowed + const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath) + if (!accessAllowed) { + await cline.say("rooignore_error", relPath) + pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath))) + return + } + + const absolutePath = path.resolve(cline.cwd, relPath) + const fileExists = await fileExistsAtPath(absolutePath) + + if (!fileExists) { + cline.consecutiveMistakeCount++ + cline.recordToolError("morph_fast_apply") + const formattedError = `File does not exist at path: ${absolutePath}\n\n\nThe specified file could not be found. Please verify the file path and try again.\n` + await cline.say("error", formattedError) + pushToolResult(formattedError) + return + } + + // Get the Morph API key from provider state + const provider = cline.providerRef.deref() + const state = await provider?.getState() + // Check if Morph API key is configured (we'll add this to provider settings later) + const morphApiKey = (state as any)?.morphApiKey + + if (!morphApiKey) { + // Morph API key not configured, fall back to regular apply_diff + await cline.say("text", "Morph API key not configured. Falling back to standard diff application.") + pushToolResult("Morph API key not configured. Please configure it in settings to use Morph Fast Apply.") + return + } + + const originalContent = await fs.readFile(absolutePath, "utf-8") + + // Call Morph API to apply the diff + const morphResult = await callMorphApi(morphApiKey, originalContent, diffContent) + + if (!morphResult.success) { + cline.consecutiveMistakeCount++ + const currentCount = (cline.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1 + cline.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount) + + TelemetryService.instance.captureDiffApplicationError(cline.taskId, currentCount) + + const formattedError = `Unable to apply diff using Morph Fast Apply: ${absolutePath}\n\n\n${morphResult.error}\n` + + if (currentCount >= 2) { + await cline.say("diff_error", formattedError) + } + + cline.recordToolError("morph_fast_apply", formattedError) + pushToolResult(formattedError) + return + } + + cline.consecutiveMistakeCount = 0 + cline.consecutiveMistakeCountForApplyDiff.delete(relPath) + + // Check if file is write-protected + const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false + + // Show diff view and ask for approval + cline.diffViewProvider.editType = "modify" + await cline.diffViewProvider.open(relPath) + await cline.diffViewProvider.update(morphResult.content!, true) + cline.diffViewProvider.scrollToFirstDiff() + + const completeMessage = JSON.stringify({ + ...sharedMessageProps, + diff: diffContent, + isProtected: isWriteProtected, + } satisfies ClineSayTool) + + const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected) + + if (!didApprove) { + await cline.diffViewProvider.revertChanges() + return + } + + // Save the changes + const diagnosticsEnabled = state?.diagnosticsEnabled ?? true + const writeDelayMs = state?.writeDelayMs ?? 0 + await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + + // Track file edit operation + await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource) + + cline.didEditFile = true + + // Get the formatted response message + const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) + + // Add success notice about using Morph + const morphNotice = "\nSuccessfully applied diff using Morph Fast Apply (98% accuracy)" + pushToolResult(message + morphNotice) + + await cline.diffViewProvider.reset() + + // Track successful Morph usage + TelemetryService.instance.captureToolUsage(cline.taskId, "morph_fast_apply") + } catch (error) { + await handleError("applying diff with Morph Fast Apply", error) + await cline.diffViewProvider.reset() + } +} diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 67972243fe..107a9c2fec 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -176,6 +176,7 @@ export const TOOL_DISPLAY_NAMES: Record = { fetch_instructions: "fetch instructions", write_to_file: "write files", apply_diff: "apply changes", + morph_fast_apply: "apply changes (Morph)", search_files: "search files", list_files: "list files", list_code_definition_names: "list definitions", @@ -205,7 +206,7 @@ export const TOOL_GROUPS: Record = { ], }, edit: { - tools: ["apply_diff", "write_to_file", "insert_content", "search_and_replace"], + tools: ["apply_diff", "morph_fast_apply", "write_to_file", "insert_content", "search_and_replace"], }, browser: { tools: ["browser_action"],