Skip to content

Commit 46e979c

Browse files
committed
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
1 parent 87c42c1 commit 46e979c

File tree

7 files changed

+273
-1
lines changed

7 files changed

+273
-1
lines changed

packages/types/src/provider-settings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ const baseProviderSettingsSchema = z.object({
9191

9292
// Model verbosity.
9393
verbosity: verbosityLevelsSchema.optional(),
94+
95+
// Morph Fast Apply API key
96+
morphApiKey: z.string().optional(),
9497
})
9598

9699
// Several of the providers share common model config properties.

packages/types/src/tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const toolNames = [
1919
"read_file",
2020
"write_to_file",
2121
"apply_diff",
22+
"morph_fast_apply",
2223
"insert_content",
2324
"search_and_replace",
2425
"search_files",

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { listFilesTool } from "../tools/listFilesTool"
1212
import { getReadFileToolDescription, readFileTool } from "../tools/readFileTool"
1313
import { writeToFileTool } from "../tools/writeToFileTool"
1414
import { applyDiffTool } from "../tools/multiApplyDiffTool"
15+
import { morphFastApplyTool } from "../tools/morphFastApplyTool"
1516
import { insertContentTool } from "../tools/insertContentTool"
1617
import { searchAndReplaceTool } from "../tools/searchAndReplaceTool"
1718
import { listCodeDefinitionNamesTool } from "../tools/listCodeDefinitionNamesTool"
@@ -179,6 +180,8 @@ export async function presentAssistantMessage(cline: Task) {
179180
}
180181
}
181182
return `[${block.name}]`
183+
case "morph_fast_apply":
184+
return `[${block.name} for '${block.params.path}']`
182185
case "search_files":
183186
return `[${block.name} for '${block.params.regex}'${
184187
block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
@@ -445,6 +448,10 @@ export async function presentAssistantMessage(cline: Task) {
445448
}
446449
break
447450
}
451+
case "morph_fast_apply":
452+
await checkpointSaveAndMark(cline)
453+
await morphFastApplyTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
454+
break
448455
case "insert_content":
449456
await checkpointSaveAndMark(cline)
450457
await insertContentTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)

src/core/prompts/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { getSwitchModeDescription } from "./switch-mode"
2323
import { getNewTaskDescription } from "./new-task"
2424
import { getCodebaseSearchDescription } from "./codebase-search"
2525
import { getUpdateTodoListDescription } from "./update-todo-list"
26+
import { getMorphFastApplyDescription } from "./morph-fast-apply"
2627
import { CodeIndexManager } from "../../../services/code-index/manager"
2728

2829
// Map of tool names to their description functions
@@ -46,6 +47,7 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
4647
search_and_replace: (args) => getSearchAndReplaceDescription(args),
4748
apply_diff: (args) =>
4849
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
50+
morph_fast_apply: (args) => getMorphFastApplyDescription(args),
4951
update_todo_list: (args) => getUpdateTodoListDescription(args),
5052
}
5153

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { ToolArgs } from "./types"
2+
3+
export function getMorphFastApplyDescription(args: ToolArgs): string | undefined {
4+
// Only show this tool if Morph API key is configured
5+
if (!args.settings?.morphApiKey) {
6+
return undefined
7+
}
8+
9+
return `## morph_fast_apply
10+
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.
11+
12+
Parameters:
13+
- path: (required) The path of the file to modify (relative to the current workspace directory ${args.cwd})
14+
- diff: (required) The diff content in SEARCH/REPLACE format
15+
16+
Diff format:
17+
\`\`\`
18+
<<<<<<< SEARCH
19+
[exact content to find including whitespace]
20+
=======
21+
[new content to replace with]
22+
>>>>>>> REPLACE
23+
\`\`\`
24+
25+
Usage:
26+
<morph_fast_apply>
27+
<path>File path here</path>
28+
<diff>
29+
Your search/replace content here
30+
</diff>
31+
</morph_fast_apply>
32+
33+
Example:
34+
<morph_fast_apply>
35+
<path>src/utils.ts</path>
36+
<diff>
37+
<<<<<<< SEARCH
38+
function calculateTotal(items) {
39+
total = 0
40+
for item in items:
41+
total += item
42+
return total
43+
=======
44+
function calculateTotal(items) {
45+
return items.reduce((sum, item) => sum + item, 0)
46+
>>>>>>> REPLACE
47+
</diff>
48+
</morph_fast_apply>
49+
50+
Note: This tool provides higher accuracy than standard apply_diff, especially for complex code transformations and when dealing with formatting variations.`
51+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import path from "path"
2+
import fs from "fs/promises"
3+
4+
import { TelemetryService } from "@roo-code/telemetry"
5+
6+
import { ClineSayTool } from "../../shared/ExtensionMessage"
7+
import { getReadablePath } from "../../utils/path"
8+
import { Task } from "../task/Task"
9+
import { ToolUse, RemoveClosingTag, AskApproval, HandleError, PushToolResult } from "../../shared/tools"
10+
import { formatResponse } from "../prompts/responses"
11+
import { fileExistsAtPath } from "../../utils/fs"
12+
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
13+
14+
interface MorphApiResponse {
15+
success: boolean
16+
content?: string
17+
error?: string
18+
}
19+
20+
/**
21+
* Calls the Morph Fast Apply API to apply diffs with high accuracy
22+
* @param apiKey The Morph API key
23+
* @param originalContent The original file content
24+
* @param diffContent The diff content to apply
25+
* @returns The result from the Morph API
26+
*/
27+
async function callMorphApi(apiKey: string, originalContent: string, diffContent: string): Promise<MorphApiResponse> {
28+
try {
29+
// Using native fetch API (available in Node.js 18+)
30+
const response = await fetch("https://api.morph.so/v1/fast-apply", {
31+
method: "POST",
32+
headers: {
33+
"Content-Type": "application/json",
34+
Authorization: `Bearer ${apiKey}`,
35+
},
36+
body: JSON.stringify({
37+
original: originalContent,
38+
diff: diffContent,
39+
}),
40+
})
41+
42+
if (!response.ok) {
43+
const errorText = await response.text()
44+
return {
45+
success: false,
46+
error: `Morph API error (${response.status}): ${errorText}`,
47+
}
48+
}
49+
50+
const data = (await response.json()) as any
51+
return {
52+
success: true,
53+
content: data.result || data.content,
54+
}
55+
} catch (error: any) {
56+
return {
57+
success: false,
58+
error: `Failed to call Morph API: ${error.message}`,
59+
}
60+
}
61+
}
62+
63+
export async function morphFastApplyTool(
64+
cline: Task,
65+
block: ToolUse,
66+
askApproval: AskApproval,
67+
handleError: HandleError,
68+
pushToolResult: PushToolResult,
69+
removeClosingTag: RemoveClosingTag,
70+
) {
71+
const relPath: string | undefined = block.params.path
72+
let diffContent: string | undefined = block.params.diff
73+
74+
const sharedMessageProps: ClineSayTool = {
75+
tool: "appliedDiff",
76+
path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
77+
diff: diffContent,
78+
}
79+
80+
try {
81+
if (block.partial) {
82+
// Update GUI message for partial blocks
83+
await cline.ask("tool", JSON.stringify(sharedMessageProps), block.partial).catch(() => {})
84+
return
85+
}
86+
87+
if (!relPath) {
88+
cline.consecutiveMistakeCount++
89+
cline.recordToolError("morph_fast_apply")
90+
pushToolResult(await cline.sayAndCreateMissingParamError("morph_fast_apply", "path"))
91+
return
92+
}
93+
94+
if (!diffContent) {
95+
cline.consecutiveMistakeCount++
96+
cline.recordToolError("morph_fast_apply")
97+
pushToolResult(await cline.sayAndCreateMissingParamError("morph_fast_apply", "diff"))
98+
return
99+
}
100+
101+
// Check if file access is allowed
102+
const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
103+
if (!accessAllowed) {
104+
await cline.say("rooignore_error", relPath)
105+
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
106+
return
107+
}
108+
109+
const absolutePath = path.resolve(cline.cwd, relPath)
110+
const fileExists = await fileExistsAtPath(absolutePath)
111+
112+
if (!fileExists) {
113+
cline.consecutiveMistakeCount++
114+
cline.recordToolError("morph_fast_apply")
115+
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>`
116+
await cline.say("error", formattedError)
117+
pushToolResult(formattedError)
118+
return
119+
}
120+
121+
// Get the Morph API key from provider state
122+
const provider = cline.providerRef.deref()
123+
const state = await provider?.getState()
124+
// Check if Morph API key is configured (we'll add this to provider settings later)
125+
const morphApiKey = (state as any)?.morphApiKey
126+
127+
if (!morphApiKey) {
128+
// Morph API key not configured, fall back to regular apply_diff
129+
await cline.say("text", "Morph API key not configured. Falling back to standard diff application.")
130+
pushToolResult("Morph API key not configured. Please configure it in settings to use Morph Fast Apply.")
131+
return
132+
}
133+
134+
const originalContent = await fs.readFile(absolutePath, "utf-8")
135+
136+
// Call Morph API to apply the diff
137+
const morphResult = await callMorphApi(morphApiKey, originalContent, diffContent)
138+
139+
if (!morphResult.success) {
140+
cline.consecutiveMistakeCount++
141+
const currentCount = (cline.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
142+
cline.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
143+
144+
TelemetryService.instance.captureDiffApplicationError(cline.taskId, currentCount)
145+
146+
const formattedError = `Unable to apply diff using Morph Fast Apply: ${absolutePath}\n\n<error_details>\n${morphResult.error}\n</error_details>`
147+
148+
if (currentCount >= 2) {
149+
await cline.say("diff_error", formattedError)
150+
}
151+
152+
cline.recordToolError("morph_fast_apply", formattedError)
153+
pushToolResult(formattedError)
154+
return
155+
}
156+
157+
cline.consecutiveMistakeCount = 0
158+
cline.consecutiveMistakeCountForApplyDiff.delete(relPath)
159+
160+
// Check if file is write-protected
161+
const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
162+
163+
// Show diff view and ask for approval
164+
cline.diffViewProvider.editType = "modify"
165+
await cline.diffViewProvider.open(relPath)
166+
await cline.diffViewProvider.update(morphResult.content!, true)
167+
cline.diffViewProvider.scrollToFirstDiff()
168+
169+
const completeMessage = JSON.stringify({
170+
...sharedMessageProps,
171+
diff: diffContent,
172+
isProtected: isWriteProtected,
173+
} satisfies ClineSayTool)
174+
175+
const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected)
176+
177+
if (!didApprove) {
178+
await cline.diffViewProvider.revertChanges()
179+
return
180+
}
181+
182+
// Save the changes
183+
const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
184+
const writeDelayMs = state?.writeDelayMs ?? 0
185+
await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
186+
187+
// Track file edit operation
188+
await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource)
189+
190+
cline.didEditFile = true
191+
192+
// Get the formatted response message
193+
const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists)
194+
195+
// Add success notice about using Morph
196+
const morphNotice = "\n<notice>Successfully applied diff using Morph Fast Apply (98% accuracy)</notice>"
197+
pushToolResult(message + morphNotice)
198+
199+
await cline.diffViewProvider.reset()
200+
201+
// Track successful Morph usage
202+
TelemetryService.instance.captureToolUsage(cline.taskId, "morph_fast_apply")
203+
} catch (error) {
204+
await handleError("applying diff with Morph Fast Apply", error)
205+
await cline.diffViewProvider.reset()
206+
}
207+
}

src/shared/tools.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
176176
fetch_instructions: "fetch instructions",
177177
write_to_file: "write files",
178178
apply_diff: "apply changes",
179+
morph_fast_apply: "apply changes (Morph)",
179180
search_files: "search files",
180181
list_files: "list files",
181182
list_code_definition_names: "list definitions",
@@ -205,7 +206,7 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
205206
],
206207
},
207208
edit: {
208-
tools: ["apply_diff", "write_to_file", "insert_content", "search_and_replace"],
209+
tools: ["apply_diff", "morph_fast_apply", "write_to_file", "insert_content", "search_and_replace"],
209210
},
210211
browser: {
211212
tools: ["browser_action"],

0 commit comments

Comments
 (0)