Просмотр исходного кода

feat: introduce experimental diff strategy toggle and enhance diff handling

- Added support for an experimental diff strategy in the Cline class, allowing users to opt for a new unified diff approach.
- Updated the getDiffStrategy function to accommodate the experimental strategy, adjusting the fuzzy match threshold accordingly.
- Integrated experimentalDiffStrategy into the global state management, enabling persistence across sessions.
- Enhanced the ClineProvider and related components to handle the new experimental strategy, including UI updates for user settings.
- Improved task history management to include the experimentalDiffStrategy setting, ensuring consistency in task execution.
- Updated relevant interfaces and types to reflect the new experimentalDiffStrategy property.
Daniel Riccio 11 месяцев назад
Родитель
Сommit
f6e85fa133

+ 15 - 10
src/core/Cline.ts

@@ -51,6 +51,7 @@ import { detectCodeOmission } from "../integrations/editor/detect-omission"
 import { BrowserSession } from "../services/browser/BrowserSession"
 import { BrowserSession } from "../services/browser/BrowserSession"
 import { OpenRouterHandler } from "../api/providers/openrouter"
 import { OpenRouterHandler } from "../api/providers/openrouter"
 import { McpHub } from "../services/mcp/McpHub"
 import { McpHub } from "../services/mcp/McpHub"
+import crypto from "crypto"
 
 
 const cwd =
 const cwd =
 	vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
 	vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
@@ -105,26 +106,30 @@ export class Cline {
 		task?: string | undefined,
 		task?: string | undefined,
 		images?: string[] | undefined,
 		images?: string[] | undefined,
 		historyItem?: HistoryItem | undefined,
 		historyItem?: HistoryItem | undefined,
+		experimentalDiffStrategy?: boolean,
 	) {
 	) {
-		this.providerRef = new WeakRef(provider)
+		this.taskId = crypto.randomUUID()
 		this.api = buildApiHandler(apiConfiguration)
 		this.api = buildApiHandler(apiConfiguration)
 		this.terminalManager = new TerminalManager()
 		this.terminalManager = new TerminalManager()
 		this.urlContentFetcher = new UrlContentFetcher(provider.context)
 		this.urlContentFetcher = new UrlContentFetcher(provider.context)
 		this.browserSession = new BrowserSession(provider.context)
 		this.browserSession = new BrowserSession(provider.context)
-		this.diffViewProvider = new DiffViewProvider(cwd)
 		this.customInstructions = customInstructions
 		this.customInstructions = customInstructions
 		this.diffEnabled = enableDiff ?? false
 		this.diffEnabled = enableDiff ?? false
-		if (this.diffEnabled && this.api.getModel().id) {
-			this.diffStrategy = getDiffStrategy(this.api.getModel().id, fuzzyMatchThreshold ?? 1.0)
-		}
+
+		// Prioritize experimentalDiffStrategy from history item if available
+		const effectiveExperimentalDiffStrategy = historyItem?.experimentalDiffStrategy ?? experimentalDiffStrategy
+		this.diffStrategy = getDiffStrategy(this.api.getModel().id, fuzzyMatchThreshold, effectiveExperimentalDiffStrategy)
+		this.diffViewProvider = new DiffViewProvider(cwd)
+		this.providerRef = new WeakRef(provider)
+
 		if (historyItem) {
 		if (historyItem) {
 			this.taskId = historyItem.id
 			this.taskId = historyItem.id
-			this.resumeTaskFromHistory()
-		} else if (task || images) {
-			this.taskId = Date.now().toString()
+		}
+
+		if (task || images) {
 			this.startTask(task, images)
 			this.startTask(task, images)
-		} else {
-			throw new Error("Either historyItem or task/images must be provided")
+		} else if (historyItem) {
+			this.resumeTaskFromHistory()
 		}
 		}
 	}
 	}
 
 

+ 8 - 4
src/core/diff/DiffStrategy.ts

@@ -7,10 +7,14 @@ import { NewUnifiedDiffStrategy } from './strategies/new-unified'
  * @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
  * @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
  * @returns The appropriate diff strategy for the model
  * @returns The appropriate diff strategy for the model
  */
  */
-export function getDiffStrategy(model: string, fuzzyMatchThreshold?: number): DiffStrategy {
-    // For now, return SearchReplaceDiffStrategy for all models
-    // This architecture allows for future optimizations based on model capabilities
-    return new NewUnifiedDiffStrategy()
+export function getDiffStrategy(model: string, fuzzyMatchThreshold?: number, experimentalDiffStrategy?: boolean): DiffStrategy {
+    if (experimentalDiffStrategy) {
+        // Use the fuzzyMatchThreshold with a minimum of 0.8 (80%)
+        const threshold = Math.max(fuzzyMatchThreshold ?? 1.0, 0.8)
+        return new NewUnifiedDiffStrategy(threshold)
+    }
+    // Default to the stable SearchReplaceDiffStrategy
+    return new SearchReplaceDiffStrategy()
 }
 }
 
 
 export type { DiffStrategy }
 export type { DiffStrategy }

+ 32 - 9
src/core/webview/ClineProvider.ts

@@ -85,6 +85,7 @@ type GlobalStateKey =
 	| "mcpEnabled"
 	| "mcpEnabled"
 	| "alwaysApproveResubmit"
 	| "alwaysApproveResubmit"
 	| "requestDelaySeconds"
 	| "requestDelaySeconds"
+	| "experimentalDiffStrategy"
 export const GlobalFileNames = {
 export const GlobalFileNames = {
 	apiConversationHistory: "api_conversation_history.json",
 	apiConversationHistory: "api_conversation_history.json",
 	uiMessages: "ui_messages.json",
 	uiMessages: "ui_messages.json",
@@ -233,7 +234,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			apiConfiguration,
 			apiConfiguration,
 			customInstructions,
 			customInstructions,
 			diffEnabled,
 			diffEnabled,
-			fuzzyMatchThreshold
+			fuzzyMatchThreshold,
+			experimentalDiffStrategy
 		} = await this.getState()
 		} = await this.getState()
 		
 		
 		this.cline = new Cline(
 		this.cline = new Cline(
@@ -243,7 +245,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			diffEnabled,
 			diffEnabled,
 			fuzzyMatchThreshold,
 			fuzzyMatchThreshold,
 			task,
 			task,
-			images
+			images,
+			undefined,
+			experimentalDiffStrategy
 		)
 		)
 	}
 	}
 
 
@@ -253,7 +257,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			apiConfiguration,
 			apiConfiguration,
 			customInstructions,
 			customInstructions,
 			diffEnabled,
 			diffEnabled,
-			fuzzyMatchThreshold
+			fuzzyMatchThreshold,
+			experimentalDiffStrategy
 		} = await this.getState()
 		} = await this.getState()
 		
 		
 		this.cline = new Cline(
 		this.cline = new Cline(
@@ -264,7 +269,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			fuzzyMatchThreshold,
 			fuzzyMatchThreshold,
 			undefined,
 			undefined,
 			undefined,
 			undefined,
-			historyItem
+			historyItem,
+			experimentalDiffStrategy
 		)
 		)
 	}
 	}
 
 
@@ -805,6 +811,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						}
 						}
 						break
 						break
 					}
 					}
+					case "experimentalDiffStrategy":
+						await this.updateGlobalState("experimentalDiffStrategy", message.bool ?? false)
+						await this.postStateToWebview()
+						break
 				}
 				}
 			},
 			},
 			null,
 			null,
@@ -1155,7 +1165,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		uiMessagesFilePath: string
 		uiMessagesFilePath: string
 		apiConversationHistory: Anthropic.MessageParam[]
 		apiConversationHistory: Anthropic.MessageParam[]
 	}> {
 	}> {
-		const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
+		const history = (await this.getGlobalState("taskHistory") as HistoryItem[] | undefined) || []
 		const historyItem = history.find((item) => item.id === id)
 		const historyItem = history.find((item) => item.id === id)
 		if (historyItem) {
 		if (historyItem) {
 			const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
 			const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
@@ -1220,7 +1230,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 
 
 	async deleteTaskFromState(id: string) {
 	async deleteTaskFromState(id: string) {
 		// Remove the task from history
 		// Remove the task from history
-		const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
+		const taskHistory = (await this.getGlobalState("taskHistory") as HistoryItem[]) || []
 		const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
 		const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
 		await this.updateGlobalState("taskHistory", updatedTaskHistory)
 		await this.updateGlobalState("taskHistory", updatedTaskHistory)
 
 
@@ -1256,6 +1266,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled,
 			mcpEnabled,
 			alwaysApproveResubmit,
 			alwaysApproveResubmit,
 			requestDelaySeconds,
 			requestDelaySeconds,
+			experimentalDiffStrategy,
 		} = await this.getState()
 		} = await this.getState()
 		
 		
 		const allowedCommands = vscode.workspace
 		const allowedCommands = vscode.workspace
@@ -1290,6 +1301,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled: mcpEnabled ?? true,
 			mcpEnabled: mcpEnabled ?? true,
 			alwaysApproveResubmit: alwaysApproveResubmit ?? false,
 			alwaysApproveResubmit: alwaysApproveResubmit ?? false,
 			requestDelaySeconds: requestDelaySeconds ?? 5,
 			requestDelaySeconds: requestDelaySeconds ?? 5,
+			experimentalDiffStrategy: experimentalDiffStrategy ?? false,
 		}
 		}
 	}
 	}
 
 
@@ -1397,6 +1409,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled,
 			mcpEnabled,
 			alwaysApproveResubmit,
 			alwaysApproveResubmit,
 			requestDelaySeconds,
 			requestDelaySeconds,
+			experimentalDiffStrategy,
 		] = await Promise.all([
 		] = await Promise.all([
 			this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
 			this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
 			this.getGlobalState("apiModelId") as Promise<string | undefined>,
 			this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1449,6 +1462,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
 			this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
 			this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
 			this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
 			this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
 			this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
+			this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
 		])
 		])
 
 
 		let apiProvider: ApiProvider
 		let apiProvider: ApiProvider
@@ -1545,16 +1559,25 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled: mcpEnabled ?? true,
 			mcpEnabled: mcpEnabled ?? true,
 			alwaysApproveResubmit: alwaysApproveResubmit ?? false,
 			alwaysApproveResubmit: alwaysApproveResubmit ?? false,
 			requestDelaySeconds: requestDelaySeconds ?? 5,
 			requestDelaySeconds: requestDelaySeconds ?? 5,
+			experimentalDiffStrategy: experimentalDiffStrategy ?? false,
 		}
 		}
 	}
 	}
 
 
 	async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
 	async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
-		const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
+		const history = (await this.getGlobalState("taskHistory") as HistoryItem[] | undefined) || []
 		const existingItemIndex = history.findIndex((h) => h.id === item.id)
 		const existingItemIndex = history.findIndex((h) => h.id === item.id)
+		
+		// Ensure experimentalDiffStrategy is included from current settings if not already set
+		const { experimentalDiffStrategy } = await this.getState() ?? {}
+		const updatedItem = {
+			...item,
+			experimentalDiffStrategy: item.experimentalDiffStrategy ?? experimentalDiffStrategy
+		}
+
 		if (existingItemIndex !== -1) {
 		if (existingItemIndex !== -1) {
-			history[existingItemIndex] = item
+			history[existingItemIndex] = updatedItem
 		} else {
 		} else {
-			history.push(item)
+			history.push(updatedItem)
 		}
 		}
 		await this.updateGlobalState("taskHistory", history)
 		await this.updateGlobalState("taskHistory", history)
 		return history
 		return history

+ 1 - 0
src/shared/ExtensionMessage.ts

@@ -70,6 +70,7 @@ export interface ExtensionState {
 	writeDelayMs: number
 	writeDelayMs: number
 	terminalOutputLineLimit?: number
 	terminalOutputLineLimit?: number
 	mcpEnabled: boolean
 	mcpEnabled: boolean
+	experimentalDiffStrategy?: boolean
 }
 }
 
 
 export interface ClineMessage {
 export interface ClineMessage {

+ 1 - 0
src/shared/HistoryItem.ts

@@ -7,4 +7,5 @@ export type HistoryItem = {
 	cacheWrites?: number
 	cacheWrites?: number
 	cacheReads?: number
 	cacheReads?: number
 	totalCost: number
 	totalCost: number
+	experimentalDiffStrategy?: boolean
 }
 }

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -54,6 +54,7 @@ export interface WebviewMessage {
 		| "searchCommits"
 		| "searchCommits"
 		| "alwaysApproveResubmit"
 		| "alwaysApproveResubmit"
 		| "requestDelaySeconds"
 		| "requestDelaySeconds"
+		| "experimentalDiffStrategy"
 	text?: string
 	text?: string
 	disabled?: boolean
 	disabled?: boolean
 	askResponse?: ClineAskResponse
 	askResponse?: ClineAskResponse

+ 24 - 2
webview-ui/src/components/settings/SettingsView.tsx

@@ -55,6 +55,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 		setAlwaysApproveResubmit,
 		setAlwaysApproveResubmit,
 		requestDelaySeconds,
 		requestDelaySeconds,
 		setRequestDelaySeconds,
 		setRequestDelaySeconds,
+		experimentalDiffStrategy,
+		setExperimentalDiffStrategy,
 	} = useExtensionState()
 	} = useExtensionState()
 	const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
 	const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
 	const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
 	const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@@ -89,6 +91,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 			vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
 			vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
 			vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
 			vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
 			vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
 			vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
+			vscode.postMessage({ type: "experimentalDiffStrategy", bool: experimentalDiffStrategy })
 			onDone()
 			onDone()
 		}
 		}
 	}
 	}
@@ -252,7 +255,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 				</div>
 				</div>
 
 
 				<div style={{ marginBottom: 5 }}>
 				<div style={{ marginBottom: 5 }}>
-					<VSCodeCheckbox checked={diffEnabled} onChange={(e: any) => setDiffEnabled(e.target.checked)}>
+					<VSCodeCheckbox checked={diffEnabled} onChange={(e: any) => {
+						setDiffEnabled(e.target.checked)
+						if (!e.target.checked) {
+							// Reset experimental strategy when diffs are disabled
+							setExperimentalDiffStrategy(false)
+						}
+					}}>
 						<span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
 						<span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
 					</VSCodeCheckbox>
 					</VSCodeCheckbox>
 					<p
 					<p
@@ -266,6 +275,19 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 
 
 					{diffEnabled && (
 					{diffEnabled && (
 						<div style={{ marginTop: 10 }}>
 						<div style={{ marginTop: 10 }}>
+							<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+								<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
+								<VSCodeCheckbox
+									checked={experimentalDiffStrategy}
+									onChange={(e: any) => setExperimentalDiffStrategy(e.target.checked)}>
+									<span style={{ fontWeight: "500" }}>Use experimental unified diff strategy</span>
+								</VSCodeCheckbox>
+							</div>
+							<p style={{ fontSize: "12px", marginBottom: 15, color: "var(--vscode-descriptionForeground)" }}>
+								Enable the experimental unified diff strategy. This strategy might reduce the number of retries caused by model errors but may cause unexpected behavior or incorrect edits.
+								Only enable if you understand the risks and are willing to carefully review all changes.
+							</p>
+
 							<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
 							<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
 								<span style={{ fontWeight: "500", minWidth: '100px' }}>Match precision</span>
 								<span style={{ fontWeight: "500", minWidth: '100px' }}>Match precision</span>
 								<input
 								<input
@@ -287,7 +309,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 									{Math.round((fuzzyMatchThreshold || 1) * 100)}%
 									{Math.round((fuzzyMatchThreshold || 1) * 100)}%
 								</span>
 								</span>
 							</div>
 							</div>
-							<p style={{ fontSize: "12px", marginBottom: 10, color: "var(--vscode-descriptionForeground)" }}>
+							<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
 								This slider controls how precisely code sections must match when applying diffs. Lower values allow more flexible matching but increase the risk of incorrect replacements. Use values below 100% with extreme caution.
 								This slider controls how precisely code sections must match when applying diffs. Lower values allow more flexible matching but increase the risk of incorrect replacements. Use values below 100% with extreme caution.
 							</p>
 							</p>
 						</div>
 						</div>

+ 7 - 2
webview-ui/src/context/ExtensionStateContext.tsx

@@ -50,6 +50,8 @@ export interface ExtensionStateContextType extends ExtensionState {
 	setAlwaysApproveResubmit: (value: boolean) => void
 	setAlwaysApproveResubmit: (value: boolean) => void
 	requestDelaySeconds: number
 	requestDelaySeconds: number
 	setRequestDelaySeconds: (value: number) => void
 	setRequestDelaySeconds: (value: number) => void
+	experimentalDiffStrategy: boolean
+	setExperimentalDiffStrategy: (value: boolean) => void
 }
 }
 
 
 export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
 export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -72,7 +74,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		terminalOutputLineLimit: 500,
 		terminalOutputLineLimit: 500,
 		mcpEnabled: true,
 		mcpEnabled: true,
 		alwaysApproveResubmit: false,
 		alwaysApproveResubmit: false,
-		requestDelaySeconds: 5
+		requestDelaySeconds: 0,
+		experimentalDiffStrategy: false,
 	})
 	})
 	const [didHydrateState, setDidHydrateState] = useState(false)
 	const [didHydrateState, setDidHydrateState] = useState(false)
 	const [showWelcome, setShowWelcome] = useState(false)
 	const [showWelcome, setShowWelcome] = useState(false)
@@ -208,7 +211,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		setTerminalOutputLineLimit: (value) => setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
 		setTerminalOutputLineLimit: (value) => setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
 		setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
 		setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
 		setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
 		setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
-		setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value }))
+		setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
+		experimentalDiffStrategy: state.experimentalDiffStrategy ?? false,
+		setExperimentalDiffStrategy: (value) => setState((prevState) => ({ ...prevState, experimentalDiffStrategy: value }))
 	}
 	}
 
 
 	return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
 	return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>