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.
This commit is contained in:
Daniel Riccio
2025-01-14 17:57:09 -05:00
parent a211927097
commit f6e85fa133
8 changed files with 89 additions and 27 deletions

View File

@@ -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 { } else if (historyItem) {
throw new Error("Either historyItem or task/images must be provided") this.resumeTaskFromHistory()
} }
} }

View File

@@ -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 { export function getDiffStrategy(model: string, fuzzyMatchThreshold?: number, experimentalDiffStrategy?: boolean): DiffStrategy {
// For now, return SearchReplaceDiffStrategy for all models if (experimentalDiffStrategy) {
// This architecture allows for future optimizations based on model capabilities // Use the fuzzyMatchThreshold with a minimum of 0.8 (80%)
return new NewUnifiedDiffStrategy() 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 }

View File

@@ -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

View File

@@ -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 {

View File

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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>