Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const toolNames = [
"read_file",
"write_to_file",
"apply_diff",
"morph_fast_apply",
"insert_content",
"search_and_replace",
"search_files",
Expand Down
7 changes: 7 additions & 0 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}'` : ""
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/core/prompts/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
}

Expand Down
51 changes: 51 additions & 0 deletions src/core/prompts/tools/morph-fast-apply.ts
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:
Copy link
Contributor Author

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?

Suggested change
for item in items:
function calculateTotal(items) {
let total = 0
for (const item of items) {
total += item
}
return total

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.`
}
207 changes: 207 additions & 0 deletions src/core/tools/morphFastApplyTool.ts
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> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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", {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using as any here removes type safety. Could we define a proper interface for the Morph API response to maintain type safety throughout the codebase?

Suggested change
const data = (await response.json()) as any
interface MorphApiResponseData {
result?: string;
content?: string;
}
const data = (await response.json()) as MorphApiResponseData

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security concern: Is this the right way to access the morphApiKey? Using (state as any)?.morphApiKey feels risky. Should we be accessing it through provider settings instead to ensure proper encapsulation and avoid potential API key exposure?


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.")
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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()
}
}
3 changes: 2 additions & 1 deletion src/shared/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"],
Expand Down
Loading