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
This commit is contained in:
Roo Code 2025-08-19 07:19:31 +00:00
parent 87c42c1f26
commit 46e979c289
7 changed files with 273 additions and 1 deletions

View file

@ -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.

View file

@ -19,6 +19,7 @@ export const toolNames = [
"read_file",
"write_to_file",
"apply_diff",
"morph_fast_apply",
"insert_content",
"search_and_replace",
"search_files",

View file

@ -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)

View file

@ -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, (args: ToolArgs) => 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),
}

View file

@ -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:
<morph_fast_apply>
<path>File path here</path>
<diff>
Your search/replace content here
</diff>
</morph_fast_apply>
Example:
<morph_fast_apply>
<path>src/utils.ts</path>
<diff>
<<<<<<< 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
</diff>
</morph_fast_apply>
Note: This tool provides higher accuracy than standard apply_diff, especially for complex code transformations and when dealing with formatting variations.`
}

View file

@ -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<MorphApiResponse> {
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<error_details>\nThe specified file could not be found. Please verify the file path and try again.\n</error_details>`
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<error_details>\n${morphResult.error}\n</error_details>`
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 = "\n<notice>Successfully applied diff using Morph Fast Apply (98% accuracy)</notice>"
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()
}
}

View file

@ -176,6 +176,7 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
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<ToolGroup, ToolGroupConfig> = {
],
},
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"],