mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Make fuzzy diff matching configurable (and default to off)
This commit is contained in:
5
.changeset/early-pigs-carry.md
Normal file
5
.changeset/early-pigs-carry.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Make fuzzy diff matching configurable (and default to off)
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface ExtensionState {
|
||||
soundVolume?: number
|
||||
diffEnabled?: boolean
|
||||
browserLargeViewport?: boolean
|
||||
fuzzyMatchThreshold?: number
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface WebviewMessage {
|
||||
| "restartMcpServer"
|
||||
| "toggleToolAlwaysAllow"
|
||||
| "toggleMcpServer"
|
||||
| "fuzzyMatchThreshold"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
askResponse?: ClineAskResponse
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user