diff --git a/CHANGELOG.md b/CHANGELOG.md index 85023ec..eeba972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Roo Cline Changelog +## [2.2.11] + +- Added settings checkbox for verbose diff debugging + ## [2.2.6 - 2.2.10] - More fixes to search/replace diffs diff --git a/package-lock.json b/package-lock.json index ae7aa41..37a56f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "2.2.10", + "version": "2.2.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "2.2.10", + "version": "2.2.11", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 98dc4bd..cd35cdc 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Cline", "description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.", "publisher": "RooVeterinaryInc", - "version": "2.2.10", + "version": "2.2.11", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 5b190d7..e98be5e 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -97,6 +97,7 @@ export class Cline { apiConfiguration: ApiConfiguration, customInstructions?: string, diffEnabled?: boolean, + debugDiffEnabled?: boolean, task?: string, images?: string[], historyItem?: HistoryItem, @@ -109,7 +110,7 @@ export class Cline { this.diffViewProvider = new DiffViewProvider(cwd) this.customInstructions = customInstructions if (diffEnabled && this.api.getModel().id) { - this.diffStrategy = getDiffStrategy(this.api.getModel().id) + this.diffStrategy = getDiffStrategy(this.api.getModel().id, debugDiffEnabled) } if (historyItem) { this.taskId = historyItem.id @@ -1237,7 +1238,12 @@ export class Cline { const originalContent = await fs.readFile(absolutePath, "utf-8") // Apply the diff to the original content - const diffResult = this.diffStrategy?.applyDiff(originalContent, diffContent) ?? { + const diffResult = this.diffStrategy?.applyDiff( + originalContent, + diffContent, + parseInt(block.params.start_line ?? ''), + parseInt(block.params.end_line ?? '') + ) ?? { success: false, error: "No diff strategy available" } diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts index f50ed4f..041298f 100644 --- a/src/core/__tests__/Cline.test.ts +++ b/src/core/__tests__/Cline.test.ts @@ -278,7 +278,8 @@ describe('Cline', () => { mockProvider, mockApiConfig, 'custom instructions', - false, + false, // diffEnabled + false, // debugDiffEnabled 'test task' ); diff --git a/src/core/assistant-message/index.ts b/src/core/assistant-message/index.ts index d10967c..241f8c7 100644 --- a/src/core/assistant-message/index.ts +++ b/src/core/assistant-message/index.ts @@ -44,6 +44,8 @@ export const toolParamNames = [ "question", "result", "diff", + "start_line", + "end_line", ] as const export type ToolParamName = (typeof toolParamNames)[number] diff --git a/src/core/diff/DiffStrategy.ts b/src/core/diff/DiffStrategy.ts index 355424e..c35ea83 100644 --- a/src/core/diff/DiffStrategy.ts +++ b/src/core/diff/DiffStrategy.ts @@ -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 { +export function getDiffStrategy(model: string, debugEnabled?: boolean): DiffStrategy { // For now, return SearchReplaceDiffStrategy for all models (with a fuzzy threshold of 0.9) // This architecture allows for future optimizations based on model capabilities - return new SearchReplaceDiffStrategy(0.9) + return new SearchReplaceDiffStrategy(0.9, debugEnabled) } export type { DiffStrategy } diff --git a/src/core/diff/strategies/search-replace.ts b/src/core/diff/strategies/search-replace.ts index a950b08..2fbfe5b 100644 --- a/src/core/diff/strategies/search-replace.ts +++ b/src/core/diff/strategies/search-replace.ts @@ -1,4 +1,7 @@ import { DiffStrategy, DiffResult } from "../types" +import { addLineNumbers } from "../../../integrations/misc/extract-text" + +const BUFFER_LINES = 5; // Number of extra context lines to show before and after matches function levenshteinDistance(a: string, b: string): number { const matrix: number[][] = []; @@ -48,10 +51,12 @@ function getSimilarity(original: string, search: string): number { export class SearchReplaceDiffStrategy implements DiffStrategy { private fuzzyThreshold: number; + public debugEnabled: boolean; - constructor(fuzzyThreshold?: number) { + constructor(fuzzyThreshold?: number, debugEnabled?: boolean) { // Default to exact matching (1.0) unless fuzzy threshold specified this.fuzzyThreshold = fuzzyThreshold ?? 1.0; + this.debugEnabled = debugEnabled ?? false; } getToolDescription(cwd: string): string { @@ -119,15 +124,11 @@ Your search/replace content here // Extract the search and replace blocks const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/); if (!match) { - // Log detailed format information - console.log('Invalid Diff Format Debug:', { - expectedFormat: "<<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE", - tip: "Make sure to include both SEARCH and REPLACE sections with correct markers" - }); + const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers` : ''; return { success: false, - error: "Invalid diff format - missing required SEARCH/REPLACE sections" + error: `Invalid diff format - missing required SEARCH/REPLACE sections${debugInfo}` }; } @@ -161,21 +162,17 @@ Your search/replace content here let bestMatchScore = 0; let bestMatchContent = ""; - if (startLine !== undefined && endLine !== undefined) { + if (startLine && endLine) { // Convert to 0-based index const exactStartIndex = startLine - 1; const exactEndIndex = endLine - 1; - if (exactStartIndex < 0 || exactEndIndex >= originalLines.length) { - // Log detailed debug information - console.log('Invalid Line Range Debug:', { - requestedRange: { start: startLine, end: endLine }, - fileBounds: { start: 1, end: originalLines.length } - }); - + if (exactStartIndex < 0 || exactEndIndex >= originalLines.length || exactStartIndex > exactEndIndex) { + const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}` : ''; + return { success: false, - error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)`, + error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)${debugInfo}`, }; } @@ -196,13 +193,13 @@ Your search/replace content here let searchStartIndex = 0; let searchEndIndex = originalLines.length; - if (startLine !== undefined || endLine !== undefined) { + if (startLine || endLine) { // Convert to 0-based index and add buffer - if (startLine !== undefined) { - searchStartIndex = Math.max(0, startLine - 6); + if (startLine) { + searchStartIndex = Math.max(0, startLine - (BUFFER_LINES + 1)); } - if (endLine !== undefined) { - searchEndIndex = Math.min(originalLines.length, endLine + 5); + if (endLine) { + searchEndIndex = Math.min(originalLines.length, endLine + BUFFER_LINES); } } @@ -224,17 +221,27 @@ Your search/replace content here // Require similarity to meet threshold if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) { const searchChunk = searchLines.join('\n'); - // Log detailed debug information to console - console.log('Search/Replace Debug Info:', { - similarity: bestMatchScore, - threshold: this.fuzzyThreshold, - searchContent: searchChunk, - bestMatch: bestMatchContent || undefined - }); + const originalContentSection = startLine !== undefined && endLine !== undefined + ? `\n\nOriginal Content:\n${addLineNumbers( + originalLines.slice( + Math.max(0, startLine - 1 - BUFFER_LINES), + Math.min(originalLines.length, endLine + BUFFER_LINES) + ).join('\n'), + Math.max(1, startLine - BUFFER_LINES) + )}` + : `\n\nOriginal Content:\n${addLineNumbers(originalLines.join('\n'))}`; + const bestMatchSection = bestMatchContent + ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}` + : `\n\nBest Match Found:\n(no match)`; + + const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine && endLine ? `lines ${startLine}-${endLine}` : 'start to end'}\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}` : ''; + + const lineRange = startLine || endLine ? + ` at ${startLine ? `start: ${startLine}` : 'start'} to ${endLine ? `end: ${endLine}` : 'end'}` : ''; return { success: false, - error: `No sufficiently similar match found${startLine !== undefined ? ` near lines ${startLine}-${endLine}` : ''} (${Math.round(bestMatchScore * 100)}% similar, needs ${Math.round(this.fuzzyThreshold * 100)}%)` + error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)${debugInfo}` }; } diff --git a/src/core/diff/types.ts b/src/core/diff/types.ts index 3957a1f..a662c47 100644 --- a/src/core/diff/types.ts +++ b/src/core/diff/types.ts @@ -13,6 +13,11 @@ export type DiffResult = }}; export interface DiffStrategy { + /** + * Whether to enable detailed debug logging + */ + debugEnabled?: boolean; + /** * Get the tool description for this diff strategy * @param cwd The current working directory diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 1ce56ae..861046e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -68,6 +68,7 @@ type GlobalStateKey = | "soundEnabled" | "soundVolume" | "diffEnabled" + | "debugDiffEnabled" | "alwaysAllowMcp" export const GlobalFileNames = { @@ -213,28 +214,11 @@ export class ClineProvider implements vscode.WebviewViewProvider { async initClineWithTask(task?: string, images?: string[]) { await this.clearTask() - const { - apiConfiguration, - customInstructions, - diffEnabled, - } = await this.getState() - - this.cline = new Cline( - this, - apiConfiguration, - customInstructions, - diffEnabled, - task, - images - ) - } - - async initClineWithHistoryItem(historyItem: HistoryItem) { - await this.clearTask() - const { - apiConfiguration, - customInstructions, + const { + apiConfiguration, + customInstructions, diffEnabled, + debugDiffEnabled, } = await this.getState() this.cline = new Cline( @@ -242,6 +226,27 @@ export class ClineProvider implements vscode.WebviewViewProvider { apiConfiguration, customInstructions, diffEnabled, + debugDiffEnabled, + task, + images + ) + } + + async initClineWithHistoryItem(historyItem: HistoryItem) { + await this.clearTask() + const { + apiConfiguration, + customInstructions, + diffEnabled, + debugDiffEnabled, + } = await this.getState() + + this.cline = new Cline( + this, + apiConfiguration, + customInstructions, + diffEnabled, + debugDiffEnabled, undefined, undefined, historyItem, @@ -609,6 +614,11 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("diffEnabled", diffEnabled) await this.postStateToWebview() break + case "debugDiffEnabled": + const debugDiffEnabled = message.bool ?? false + await this.updateGlobalState("debugDiffEnabled", debugDiffEnabled) + await this.postStateToWebview() + break } }, null, @@ -935,6 +945,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowMcp, soundEnabled, diffEnabled, + debugDiffEnabled, taskHistory, soundVolume, } = await this.getState() @@ -959,6 +970,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { .sort((a, b) => b.ts - a.ts), soundEnabled: soundEnabled ?? false, diffEnabled: diffEnabled ?? false, + debugDiffEnabled: debugDiffEnabled ?? false, shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId, allowedCommands, soundVolume: soundVolume ?? 0.5, @@ -1054,6 +1066,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { allowedCommands, soundEnabled, diffEnabled, + debugDiffEnabled, soundVolume, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, @@ -1092,6 +1105,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("allowedCommands") as Promise, this.getGlobalState("soundEnabled") as Promise, this.getGlobalState("diffEnabled") as Promise, + this.getGlobalState("debugDiffEnabled") as Promise, this.getGlobalState("soundVolume") as Promise, ]) @@ -1146,8 +1160,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowMcp: alwaysAllowMcp ?? false, taskHistory, allowedCommands, - soundEnabled, - diffEnabled, + soundEnabled: soundEnabled ?? false, + diffEnabled: diffEnabled ?? false, + debugDiffEnabled: debugDiffEnabled ?? false, soundVolume, } } diff --git a/src/integrations/misc/__tests__/extract-text.test.ts b/src/integrations/misc/__tests__/extract-text.test.ts new file mode 100644 index 0000000..89adbb1 --- /dev/null +++ b/src/integrations/misc/__tests__/extract-text.test.ts @@ -0,0 +1,32 @@ +import { addLineNumbers } from '../extract-text'; + +describe('addLineNumbers', () => { + it('should add line numbers starting from 1 by default', () => { + const input = 'line 1\nline 2\nline 3'; + const expected = '1 | line 1\n2 | line 2\n3 | line 3'; + expect(addLineNumbers(input)).toBe(expected); + }); + + it('should add line numbers starting from specified line number', () => { + const input = 'line 1\nline 2\nline 3'; + const expected = '10 | line 1\n11 | line 2\n12 | line 3'; + expect(addLineNumbers(input, 10)).toBe(expected); + }); + + it('should handle empty content', () => { + expect(addLineNumbers('')).toBe('1 | '); + expect(addLineNumbers('', 5)).toBe('5 | '); + }); + + it('should handle single line content', () => { + expect(addLineNumbers('single line')).toBe('1 | single line'); + expect(addLineNumbers('single line', 42)).toBe('42 | single line'); + }); + + it('should pad line numbers based on the highest line number', () => { + const input = 'line 1\nline 2'; + // When starting from 99, highest line will be 100, so needs 3 spaces padding + const expected = ' 99 | line 1\n100 | line 2'; + expect(addLineNumbers(input, 99)).toBe(expected); + }); +}); \ No newline at end of file diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts index 3f9ff1c..576194c 100644 --- a/src/integrations/misc/extract-text.ts +++ b/src/integrations/misc/extract-text.ts @@ -53,15 +53,12 @@ async function extractTextFromIPYNB(filePath: string): Promise { return addLineNumbers(extractedText) } - -export function addLineNumbers(content: string): string { +export function addLineNumbers(content: string, startLine: number = 1): string { const lines = content.split('\n') - const maxLineNumberWidth = String(lines.length).length + const maxLineNumberWidth = String(startLine + lines.length - 1).length return lines .map((line, index) => { - const lineNumber = String(index + 1).padStart(maxLineNumberWidth, ' ') + const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ') return `${lineNumber} | ${line}` }).join('\n') -} - - +} \ No newline at end of file diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 07a3dde..e95bb80 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -53,6 +53,7 @@ export interface ExtensionState { soundEnabled?: boolean soundVolume?: number diffEnabled?: boolean + debugDiffEnabled?: boolean } export interface ClineMessage { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 2864a94..aca93e1 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -34,6 +34,7 @@ export interface WebviewMessage { | "soundEnabled" | "soundVolume" | "diffEnabled" + | "debugDiffEnabled" | "openMcpSettings" | "restartMcpServer" | "toggleToolAlwaysAllow" diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 2c3fea8..e1c9701 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -33,6 +33,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { setSoundVolume, diffEnabled, setDiffEnabled, + debugDiffEnabled, + setDebugDiffEnabled, openRouterModels, setAllowedCommands, allowedCommands, @@ -48,7 +50,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { setApiErrorMessage(apiValidationResult) setModelIdErrorMessage(modelIdValidationResult) if (!apiValidationResult && !modelIdValidationResult) { - vscode.postMessage({ type: "apiConfiguration", apiConfiguration }) + vscode.postMessage({ + type: "apiConfiguration", + apiConfiguration + }) vscode.postMessage({ type: "customInstructions", text: customInstructions }) vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly }) vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite }) @@ -59,6 +64,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { vscode.postMessage({ type: "soundEnabled", bool: soundEnabled }) vscode.postMessage({ type: "soundVolume", value: soundVolume }) vscode.postMessage({ type: "diffEnabled", bool: diffEnabled }) + vscode.postMessage({ type: "debugDiffEnabled", bool: debugDiffEnabled }) onDone() } } @@ -158,7 +164,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { marginTop: "5px", color: "var(--vscode-descriptionForeground)", }}> - When enabled, Cline will be able to edit files more quickly and will automatically reject truncated full-file writes. + 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.

@@ -352,6 +358,20 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { )} + +
+ setDebugDiffEnabled(e.target.checked)}> + Debug diff operations + +

+ When enabled, Cline will show detailed debug information when applying diffs fails. +

+
{IS_DEV && ( diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index c8307ff..f4fdaa3 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -31,6 +31,7 @@ export interface ExtensionStateContextType extends ExtensionState { setSoundEnabled: (value: boolean) => void setSoundVolume: (value: number) => void setDiffEnabled: (value: boolean) => void + setDebugDiffEnabled: (value: boolean) => void } const ExtensionStateContext = createContext(undefined) @@ -45,6 +46,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode soundEnabled: false, soundVolume: 0.5, diffEnabled: false, + debugDiffEnabled: false, }) const [didHydrateState, setDidHydrateState] = useState(false) const [showWelcome, setShowWelcome] = useState(false) @@ -132,7 +134,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode mcpServers, filePaths, soundVolume: state.soundVolume, - setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })), + setApiConfiguration: (value) => setState((prevState) => ({ + ...prevState, + apiConfiguration: value + })), setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })), setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })), setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })), @@ -144,6 +149,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })), setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })), setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })), + setDebugDiffEnabled: (value) => setState((prevState) => ({ + ...prevState, + debugDiffEnabled: value + })), } return {children}