Merge pull request #169 from RooVetGit/turn_off_fuzzy_matching

Make fuzzy diff matching configurable (and default to off)
This commit is contained in:
Matt Rubens
2024-12-18 17:15:25 -05:00
committed by GitHub
10 changed files with 137 additions and 21 deletions

View File

@@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Make fuzzy diff matching configurable (and default to off)

View File

@@ -67,6 +67,7 @@ export class Cline {
private didEditFile: boolean = false
customInstructions?: string
diffStrategy?: DiffStrategy
diffEnabled: boolean = false
apiConversationHistory: Anthropic.MessageParam[] = []
clineMessages: ClineMessage[] = []
@@ -97,10 +98,11 @@ export class Cline {
provider: ClineProvider,
apiConfiguration: ApiConfiguration,
customInstructions?: string,
diffEnabled?: boolean,
task?: string,
images?: string[],
historyItem?: HistoryItem,
enableDiff?: boolean,
fuzzyMatchThreshold?: number,
task?: string | undefined,
images?: string[] | undefined,
historyItem?: HistoryItem | undefined,
) {
this.providerRef = new WeakRef(provider)
this.api = buildApiHandler(apiConfiguration)
@@ -109,8 +111,9 @@ export class Cline {
this.browserSession = new BrowserSession(provider.context)
this.diffViewProvider = new DiffViewProvider(cwd)
this.customInstructions = customInstructions
if (diffEnabled && this.api.getModel().id) {
this.diffStrategy = getDiffStrategy(this.api.getModel().id)
this.diffEnabled = enableDiff ?? false
if (this.diffEnabled && this.api.getModel().id) {
this.diffStrategy = getDiffStrategy(this.api.getModel().id, fuzzyMatchThreshold ?? 1.0)
}
if (historyItem) {
this.taskId = historyItem.id

View File

@@ -248,7 +248,7 @@ describe('Cline', () => {
// Setup mock API configuration
mockApiConfig = {
apiProvider: 'anthropic',
apiModelId: 'claude-3-sonnet'
apiModelId: 'claude-3-5-sonnet-20241022'
};
// Mock provider methods
@@ -278,20 +278,77 @@ describe('Cline', () => {
mockProvider,
mockApiConfig,
'custom instructions',
false, // diffEnabled
'test task', // task
undefined, // images
undefined // historyItem
false,
0.95, // 95% threshold
'test task'
);
expect(cline.customInstructions).toBe('custom instructions');
expect(cline.diffEnabled).toBe(false);
});
it('should use default fuzzy match threshold when not provided', () => {
const cline = new Cline(
mockProvider,
mockApiConfig,
'custom instructions',
true,
undefined,
'test task'
);
expect(cline.diffEnabled).toBe(true);
// The diff strategy should be created with default threshold (1.0)
expect(cline.diffStrategy).toBeDefined();
});
it('should use provided fuzzy match threshold', () => {
const getDiffStrategySpy = jest.spyOn(require('../diff/DiffStrategy'), 'getDiffStrategy');
const cline = new Cline(
mockProvider,
mockApiConfig,
'custom instructions',
true,
0.9, // 90% threshold
'test task'
);
expect(cline.diffEnabled).toBe(true);
expect(cline.diffStrategy).toBeDefined();
expect(getDiffStrategySpy).toHaveBeenCalledWith('claude-3-5-sonnet-20241022', 0.9);
getDiffStrategySpy.mockRestore();
});
it('should pass default threshold to diff strategy when not provided', () => {
const getDiffStrategySpy = jest.spyOn(require('../diff/DiffStrategy'), 'getDiffStrategy');
const cline = new Cline(
mockProvider,
mockApiConfig,
'custom instructions',
true,
undefined,
'test task'
);
expect(cline.diffEnabled).toBe(true);
expect(cline.diffStrategy).toBeDefined();
expect(getDiffStrategySpy).toHaveBeenCalledWith('claude-3-5-sonnet-20241022', 1.0);
getDiffStrategySpy.mockRestore();
});
it('should require either task or historyItem', () => {
expect(() => {
new Cline(
mockProvider,
mockApiConfig
mockApiConfig,
undefined, // customInstructions
false, // diffEnabled
undefined, // fuzzyMatchThreshold
undefined // task
);
}).toThrow('Either historyItem or task/images must be provided');
});

View File

@@ -6,10 +6,10 @@ import { SearchReplaceDiffStrategy } from './strategies/search-replace'
* @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): DiffStrategy {
// For now, return SearchReplaceDiffStrategy for all models (with a fuzzy threshold of 0.9)
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 SearchReplaceDiffStrategy(0.9)
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold ?? 1.0)
}
export type { DiffStrategy }

View File

@@ -58,7 +58,9 @@ export class SearchReplaceDiffStrategy implements DiffStrategy {
private bufferLines: number;
constructor(fuzzyThreshold?: number, bufferLines?: number) {
// Default to exact matching (1.0) unless fuzzy threshold specified
// Use provided threshold or default to exact matching (1.0)
// Note: fuzzyThreshold is inverted in UI (0% = 1.0, 10% = 0.9)
// so we use it directly here
this.fuzzyThreshold = fuzzyThreshold ?? 1.0;
this.bufferLines = bufferLines ?? BUFFER_LINES;
}

View File

@@ -70,6 +70,7 @@ type GlobalStateKey =
| "diffEnabled"
| "alwaysAllowMcp"
| "browserLargeViewport"
| "fuzzyMatchThreshold"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@@ -217,7 +218,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const {
apiConfiguration,
customInstructions,
diffEnabled
diffEnabled,
fuzzyMatchThreshold
} = await this.getState()
this.cline = new Cline(
@@ -225,6 +227,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customInstructions,
diffEnabled,
fuzzyMatchThreshold,
task,
images
)
@@ -235,7 +238,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const {
apiConfiguration,
customInstructions,
diffEnabled
diffEnabled,
fuzzyMatchThreshold
} = await this.getState()
this.cline = new Cline(
@@ -243,6 +247,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customInstructions,
diffEnabled,
fuzzyMatchThreshold,
undefined,
undefined,
historyItem
@@ -613,6 +618,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("browserLargeViewport", browserLargeViewport)
await this.postStateToWebview()
break
case "fuzzyMatchThreshold":
await this.updateGlobalState("fuzzyMatchThreshold", message.value)
await this.postStateToWebview()
break
}
},
null,
@@ -1062,6 +1071,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
diffEnabled,
soundVolume,
browserLargeViewport,
fuzzyMatchThreshold,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1101,6 +1111,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("diffEnabled") as Promise<boolean | undefined>,
this.getGlobalState("soundVolume") as Promise<number | undefined>,
this.getGlobalState("browserLargeViewport") as Promise<boolean | undefined>,
this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
])
let apiProvider: ApiProvider
@@ -1158,6 +1169,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
diffEnabled: diffEnabled ?? false,
soundVolume,
browserLargeViewport: browserLargeViewport ?? false,
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
}
}

View File

@@ -54,6 +54,7 @@ export interface ExtensionState {
soundVolume?: number
diffEnabled?: boolean
browserLargeViewport?: boolean
fuzzyMatchThreshold?: number
}
export interface ClineMessage {

View File

@@ -39,6 +39,7 @@ export interface WebviewMessage {
| "restartMcpServer"
| "toggleToolAlwaysAllow"
| "toggleMcpServer"
| "fuzzyMatchThreshold"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse

View File

@@ -33,16 +33,17 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setSoundVolume,
diffEnabled,
setDiffEnabled,
browserLargeViewport = false,
browserLargeViewport,
setBrowserLargeViewport,
openRouterModels,
setAllowedCommands,
allowedCommands,
fuzzyMatchThreshold,
setFuzzyMatchThreshold,
} = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
const [commandInput, setCommandInput] = useState("")
const handleSubmit = () => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
@@ -65,6 +66,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
vscode.postMessage({ type: "soundVolume", value: soundVolume })
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
vscode.postMessage({ type: "browserLargeViewport", bool: browserLargeViewport })
vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
onDone()
}
}
@@ -166,6 +168,35 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
}}>
When enabled, Cline will be able to edit files more quickly and will automatically reject truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model.
</p>
{diffEnabled && (
<div style={{ marginTop: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ fontWeight: "500", minWidth: '100px' }}>Match precision</span>
<input
type="range"
min="0.9"
max="1"
step="0.005"
value={fuzzyMatchThreshold ?? 1.0}
onChange={(e) => {
setFuzzyMatchThreshold(parseFloat(e.target.value));
}}
style={{
flexGrow: 1,
accentColor: 'var(--vscode-button-background)',
height: '2px'
}}
/>
<span style={{ minWidth: '35px', textAlign: 'left' }}>
{Math.round((fuzzyMatchThreshold || 1) * 100)}%
</span>
</div>
<p style={{ fontSize: "12px", marginBottom: 10, 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>
)}
</div>
<div style={{ marginBottom: 5 }}>
@@ -351,7 +382,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
{soundEnabled && (
<div style={{ marginLeft: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ fontWeight: "500", minWidth: '50px' }}>Volume</span>
<span style={{ fontWeight: "500", minWidth: '100px' }}>Volume</span>
<input
type="range"
min="0"

View File

@@ -32,6 +32,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setSoundVolume: (value: number) => void
setDiffEnabled: (value: boolean) => void
setBrowserLargeViewport: (value: boolean) => void
setFuzzyMatchThreshold: (value: number) => void
}
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -46,6 +47,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
soundEnabled: false,
soundVolume: 0.5,
diffEnabled: false,
fuzzyMatchThreshold: 1.0,
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
@@ -133,6 +135,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
mcpServers,
filePaths,
soundVolume: state.soundVolume,
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
setApiConfiguration: (value) => setState((prevState) => ({
...prevState,
apiConfiguration: value
@@ -149,6 +152,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
setBrowserLargeViewport: (value) => setState((prevState) => ({ ...prevState, browserLargeViewport: value })),
setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>