diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts
index be74ae6bb4..f4c0fcb9b2 100644
--- a/packages/types/src/provider-settings.ts
+++ b/packages/types/src/provider-settings.ts
@@ -71,6 +71,14 @@ const baseProviderSettingsSchema = z.object({
reasoningEffort: reasoningEffortsSchema.optional(),
modelMaxTokens: z.number().optional(),
modelMaxThinkingTokens: z.number().optional(),
+
+ // Apply model configuration for separate diff application
+ applyModelEnabled: z.boolean().optional(),
+ applyModelProvider: providerNamesSchema.optional(),
+ applyModelId: z.string().optional(),
+ applyModelApiKey: z.string().optional(),
+ applyModelBaseUrl: z.string().optional(),
+ applyModelTemperature: z.number().nullish(),
})
// Several of the providers share common model config properties.
diff --git a/src/core/diff/strategies/__tests__/apply-model-diff-strategy.test.ts b/src/core/diff/strategies/__tests__/apply-model-diff-strategy.test.ts
new file mode 100644
index 0000000000..b38a5efc8d
--- /dev/null
+++ b/src/core/diff/strategies/__tests__/apply-model-diff-strategy.test.ts
@@ -0,0 +1,116 @@
+import { describe, it, expect, vi, beforeEach } from "vitest"
+import { ApplyModelDiffStrategy } from "../apply-model-diff-strategy"
+import { Task } from "../../../task/Task"
+
+// Mock the Task class
+const mockTask = {
+ providerRef: {
+ deref: () => ({
+ getSettings: () => ({
+ applyModelEnabled: false,
+ applyModelProvider: "openai",
+ applyModelId: "gpt-4",
+ applyModelApiKey: "test-key",
+ applyModelBaseUrl: "https://api.openai.com/v1",
+ applyModelTemperature: 0.1,
+ }),
+ }),
+ },
+} as unknown as Task
+
+describe("ApplyModelDiffStrategy", () => {
+ let strategy: ApplyModelDiffStrategy
+
+ beforeEach(() => {
+ strategy = new ApplyModelDiffStrategy(mockTask, 1.0)
+ })
+
+ it("should have correct name", () => {
+ expect(strategy.getName()).toBe("ApplyModel")
+ })
+
+ it("should return tool description", () => {
+ const description = strategy.getToolDescription({ cwd: "/test" })
+ expect(description).toContain("apply_diff")
+ expect(description).toContain("AI-powered apply model")
+ expect(description).toContain("/test")
+ })
+
+ it("should handle disabled apply model", async () => {
+ const originalContent = "function test() { return 1; }"
+ const changeDescription = "Add a comment to the function"
+
+ const result = await strategy.applyDiff(originalContent, changeDescription)
+
+ expect(result.success).toBe(false)
+ if (!result.success) {
+ expect(result.error).toContain("Apply model is not enabled")
+ }
+ })
+
+ it("should handle array-based diff content", async () => {
+ const originalContent = "function test() { return 1; }"
+ const diffItems = [
+ { content: "Add a comment to the function", startLine: 1 },
+ { content: "Add error handling", startLine: 2 },
+ ]
+
+ const result = await strategy.applyDiff(originalContent, diffItems)
+
+ expect(result.success).toBe(false)
+ if (!result.success) {
+ expect(result.error).toContain("Apply model is not enabled")
+ }
+ })
+
+ it("should return progress status", () => {
+ const toolUse = {
+ type: "tool_use" as const,
+ name: "apply_diff" as const,
+ params: { diff: "test changes" },
+ partial: false,
+ }
+
+ const status = strategy.getProgressStatus(toolUse)
+ expect(status).toHaveProperty("icon", "wand")
+ })
+
+ it("should handle partial tool use", () => {
+ const toolUse = {
+ type: "tool_use" as const,
+ name: "apply_diff" as const,
+ params: { diff: "test changes" },
+ partial: true,
+ }
+
+ const status = strategy.getProgressStatus(toolUse)
+ expect(status).toHaveProperty("icon", "wand")
+ expect(status).toHaveProperty("text", "Analyzing...")
+ })
+
+ it("should handle successful result", () => {
+ const toolUse = {
+ type: "tool_use" as const,
+ name: "apply_diff" as const,
+ params: { diff: "test changes" },
+ partial: false,
+ }
+
+ const result = { success: true as const, content: "modified content" }
+ const status = strategy.getProgressStatus(toolUse, result)
+ expect(status).toHaveProperty("text", "Applied")
+ })
+
+ it("should handle failed result", () => {
+ const toolUse = {
+ type: "tool_use" as const,
+ name: "apply_diff" as const,
+ params: { diff: "test changes" },
+ partial: false,
+ }
+
+ const result = { success: false as const, error: "test error" }
+ const status = strategy.getProgressStatus(toolUse, result)
+ expect(status).toHaveProperty("text", "Failed")
+ })
+})
\ No newline at end of file
diff --git a/src/core/diff/strategies/apply-model-diff-strategy.ts b/src/core/diff/strategies/apply-model-diff-strategy.ts
new file mode 100644
index 0000000000..0a34578f6b
--- /dev/null
+++ b/src/core/diff/strategies/apply-model-diff-strategy.ts
@@ -0,0 +1,173 @@
+import { ToolProgressStatus } from "@roo-code/types"
+
+import { ToolUse, DiffStrategy, DiffResult, DiffItem } from "../../../shared/tools"
+import { Task } from "../../task/Task"
+import { ProviderSettings } from "@roo-code/types"
+
+/**
+ * ApplyModelDiffStrategy uses a separate "apply" model to generate and apply diffs
+ * instead of having the main chat model generate the diff format.
+ * This reduces token consumption and improves reliability for file edits.
+ */
+export class ApplyModelDiffStrategy implements DiffStrategy {
+ private task: Task
+ private fuzzyThreshold: number
+
+ constructor(task: Task, fuzzyThreshold?: number) {
+ this.task = task
+ this.fuzzyThreshold = fuzzyThreshold ?? 1.0
+ }
+
+ getName(): string {
+ return "ApplyModel"
+ }
+
+ getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
+ return `## apply_diff
+
+Description: Request to apply targeted modifications to an existing file using an AI-powered apply model. This tool uses a separate specialized model to understand your intent and apply changes directly to the file content, reducing the need for precise diff formatting and improving reliability.
+
+The apply model will analyze the original file content and your change description to generate the appropriate modifications automatically.
+
+Parameters:
+- path: (required) The path of the file to modify (relative to the current workspace directory ${args.cwd})
+- changes: (required) A description of the changes you want to make to the file. Be specific about what you want to change, add, or remove.
+
+Usage:
+
+File path here
+
+Describe the changes you want to make to the file. For example:
+- "Add a new function called calculateTotal that takes an array of numbers and returns their sum"
+- "Update the existing validateUser function to also check for email format"
+- "Remove the deprecated legacy_function and replace its usage with new_function"
+- "Add error handling to the database connection code"
+
+
+
+Example:
+
+
+src/utils.ts
+
+Add a new function called formatCurrency that takes a number and returns a formatted currency string with dollar sign and two decimal places. Place it after the existing formatDate function.
+
+`
+ }
+
+ async applyDiff(
+ originalContent: string,
+ diffContent: string | DiffItem[],
+ _paramStartLine?: number,
+ _paramEndLine?: number,
+ ): Promise {
+ // Handle array-based input (from multi-file operations)
+ if (Array.isArray(diffContent)) {
+ // For array input, combine all change descriptions
+ const combinedChanges = diffContent.map(item => item.content).join('\n\n')
+ return this.applyChangesWithModel(originalContent, combinedChanges)
+ }
+
+ // Handle string-based input (legacy and single operations)
+ return this.applyChangesWithModel(originalContent, diffContent)
+ }
+
+ private async applyChangesWithModel(originalContent: string, changeDescription: string): Promise {
+ try {
+ // Get apply model configuration from task settings
+ const applyModelConfig = this.getApplyModelConfig()
+ if (!applyModelConfig.enabled) {
+ return {
+ success: false,
+ error: "Apply model is not enabled. Please configure an apply model in settings."
+ }
+ }
+
+ // Create a prompt for the apply model
+ const prompt = this.createApplyPrompt(originalContent, changeDescription)
+
+ // Call the apply model to generate the modified content
+ const modifiedContent = await this.callApplyModel(prompt, applyModelConfig)
+
+ // Validate that the content was actually changed
+ if (modifiedContent === originalContent) {
+ return {
+ success: false,
+ error: "Apply model returned unchanged content. The requested changes may not be applicable or clear enough."
+ }
+ }
+
+ return {
+ success: true,
+ content: modifiedContent
+ }
+ } catch (error) {
+ return {
+ success: false,
+ error: `Apply model failed: ${error instanceof Error ? error.message : String(error)}`
+ }
+ }
+ }
+
+ private getApplyModelConfig() {
+ // Get apply model configuration from task's provider settings
+ // For now, we'll use a placeholder implementation
+ // In the full implementation, this would access the provider settings
+ return {
+ enabled: false, // Will be updated when provider integration is complete
+ provider: undefined,
+ modelId: undefined,
+ apiKey: undefined,
+ baseUrl: undefined,
+ temperature: 0.1, // Low temperature for consistent edits
+ }
+ }
+
+ private createApplyPrompt(originalContent: string, changeDescription: string): string {
+ return `You are an expert code editor. Your task is to apply the requested changes to the provided file content.
+
+IMPORTANT INSTRUCTIONS:
+1. Apply ONLY the changes described in the change request
+2. Preserve all existing code structure, formatting, and style
+3. Do not add comments about what you changed
+4. Return the complete modified file content
+5. If the change cannot be applied, return the original content unchanged
+
+ORIGINAL FILE CONTENT:
+\`\`\`
+${originalContent}
+\`\`\`
+
+REQUESTED CHANGES:
+${changeDescription}
+
+MODIFIED FILE CONTENT:`
+ }
+
+ private async callApplyModel(prompt: string, config: any): Promise {
+ // This is a simplified implementation. In a real implementation, you would:
+ // 1. Create an API client for the specified provider
+ // 2. Make the API call with the prompt
+ // 3. Parse and return the response
+
+ // For now, we'll throw an error to indicate this needs to be implemented
+ throw new Error("Apply model API integration not yet implemented. This feature requires connecting to external apply models like Morph Fast Apply or Relace's Instant Apply.")
+ }
+
+ getProgressStatus(toolUse: ToolUse, result?: DiffResult): ToolProgressStatus {
+ const changes = toolUse.params.diff // Use 'diff' parameter which exists in the ToolUse interface
+ if (changes) {
+ const icon = "wand"
+ if (toolUse.partial) {
+ return { icon, text: "Analyzing..." }
+ } else if (result) {
+ if (result.success) {
+ return { icon, text: "Applied" }
+ } else {
+ return { icon, text: "Failed" }
+ }
+ }
+ }
+ return {}
+ }
+}
\ No newline at end of file
diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts
index 53b8ef5b87..e28e76e623 100644
--- a/src/core/task/Task.ts
+++ b/src/core/task/Task.ts
@@ -76,6 +76,7 @@ import { truncateConversationIfNeeded } from "../sliding-window"
import { ClineProvider } from "../webview/ClineProvider"
import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-replace"
import { MultiFileSearchReplaceDiffStrategy } from "../diff/strategies/multi-file-search-replace"
+import { ApplyModelDiffStrategy } from "../diff/strategies/apply-model-diff-strategy"
import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "../task-persistence"
import { getEnvironmentDetails } from "../environment/getEnvironmentDetails"
import {
@@ -280,13 +281,18 @@ export class Task extends EventEmitter {
// Check experiment asynchronously and update strategy if needed
provider.getState().then((state) => {
- const isMultiFileApplyDiffEnabled = experiments.isEnabled(
- state.experiments ?? {},
- EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF,
- )
+ // Check if apply model is enabled first
+ if (apiConfiguration.applyModelEnabled) {
+ this.diffStrategy = new ApplyModelDiffStrategy(this, this.fuzzyMatchThreshold)
+ } else {
+ const isMultiFileApplyDiffEnabled = experiments.isEnabled(
+ state.experiments ?? {},
+ EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF,
+ )
- if (isMultiFileApplyDiffEnabled) {
- this.diffStrategy = new MultiFileSearchReplaceDiffStrategy(this.fuzzyMatchThreshold)
+ if (isMultiFileApplyDiffEnabled) {
+ this.diffStrategy = new MultiFileSearchReplaceDiffStrategy(this.fuzzyMatchThreshold)
+ }
}
})
}