-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat: add optional Morph Fast Apply tool for improved diff accuracy #7208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.` | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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> { | ||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing rate limiting and retry logic for API calls. If the tool is used frequently, could we hit rate limits? Should we add exponential backoff or at least basic retry logic for transient failures? |
||||||||||||||||
| try { | ||||||||||||||||
| // Using native fetch API (available in Node.js 18+) | ||||||||||||||||
| const response = await fetch("https://api.morph.so/v1/fast-apply", { | ||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The API endpoint is hardcoded here. Should we make this configurable for different environments (dev/staging/prod) or add a fallback URL in case the primary endpoint is down? |
||||||||||||||||
| 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 | ||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Suggested change
|
||||||||||||||||
| 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 | ||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security concern: Is this the right way to access the morphApiKey? Using |
||||||||||||||||
|
|
||||||||||||||||
| 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.") | ||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the API key isn't configured, we show one message to the user but return a different message in the result. Should these be consistent for better UX? |
||||||||||||||||
| 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() | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example mixes Python-like syntax (
for item in items:) with JavaScript. Is this intentional, or should we use consistent JavaScript syntax?