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 { OpenRouterHandler } from "../api/providers/openrouter"
import { McpHub } from "../services/mcp/McpHub"
import crypto from "crypto"
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
@@ -105,26 +106,30 @@ export class Cline {
task?: string | undefined,
images?: string[] | undefined,
historyItem?: HistoryItem | undefined,
experimentalDiffStrategy?: boolean,
) {
this.providerRef = new WeakRef(provider)
this.taskId = crypto.randomUUID()
this.api = buildApiHandler(apiConfiguration)
this.terminalManager = new TerminalManager()
this.urlContentFetcher = new UrlContentFetcher(provider.context)
this.browserSession = new BrowserSession(provider.context)
this.diffViewProvider = new DiffViewProvider(cwd)
this.customInstructions = customInstructions
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) {
this.taskId = historyItem.id
this.resumeTaskFromHistory()
} else if (task || images) {
this.taskId = Date.now().toString()
}
if (task || images) {
this.startTask(task, images)
} else {
throw new Error("Either historyItem or task/images must be provided")
} else if (historyItem) {
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')
* @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 }

View File

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

View File

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

View File

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

View File

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

View File

@@ -55,6 +55,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setAlwaysApproveResubmit,
requestDelaySeconds,
setRequestDelaySeconds,
experimentalDiffStrategy,
setExperimentalDiffStrategy,
} = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = 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: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
vscode.postMessage({ type: "experimentalDiffStrategy", bool: experimentalDiffStrategy })
onDone()
}
}
@@ -252,7 +255,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</div>
<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>
</VSCodeCheckbox>
<p
@@ -266,6 +275,19 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
{diffEnabled && (
<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' }}>
<span style={{ fontWeight: "500", minWidth: '100px' }}>Match precision</span>
<input
@@ -287,7 +309,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
{Math.round((fuzzyMatchThreshold || 1) * 100)}%
</span>
</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.
</p>
</div>

View File

@@ -50,6 +50,8 @@ export interface ExtensionStateContextType extends ExtensionState {
setAlwaysApproveResubmit: (value: boolean) => void
requestDelaySeconds: number
setRequestDelaySeconds: (value: number) => void
experimentalDiffStrategy: boolean
setExperimentalDiffStrategy: (value: boolean) => void
}
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -72,7 +74,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
terminalOutputLineLimit: 500,
mcpEnabled: true,
alwaysApproveResubmit: false,
requestDelaySeconds: 5
requestDelaySeconds: 0,
experimentalDiffStrategy: false,
})
const [didHydrateState, setDidHydrateState] = 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 })),
setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: 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>