Răsfoiți Sursa

Merge pull request #132 from RooVetGit/debug_diff_enabled

Debug diff enabled
Matt Rubens 1 an în urmă
părinte
comite
47568336ad

+ 4 - 0
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

+ 2 - 2
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",

+ 1 - 1
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",

+ 8 - 2
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"
 								}

+ 2 - 1
src/core/__tests__/Cline.test.ts

@@ -278,7 +278,8 @@ describe('Cline', () => {
                 mockProvider,
                 mockApiConfig,
                 'custom instructions',
-                false,
+                false, // diffEnabled
+                false, // debugDiffEnabled
                 'test task'
             );
 

+ 2 - 0
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]

+ 2 - 2
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 }

+ 37 - 30
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}`
             };
         }
 

+ 5 - 0
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

+ 27 - 12
src/core/webview/ClineProvider.ts

@@ -67,6 +67,7 @@ type GlobalStateKey =
 	| "allowedCommands"
 	| "soundEnabled"
 	| "diffEnabled"
+	| "debugDiffEnabled"
 	| "alwaysAllowMcp"
 
 export const GlobalFileNames = {
@@ -207,28 +208,31 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 
 	async initClineWithTask(task?: string, images?: string[]) {
 		await this.clearTask()
-		const { 
-			apiConfiguration, 
-			customInstructions, 
+		const {
+			apiConfiguration,
+			customInstructions,
 			diffEnabled,
+			debugDiffEnabled,
 		} = await this.getState()
 		
 		this.cline = new Cline(
-			this, 
-			apiConfiguration, 
-			customInstructions, 
+			this,
+			apiConfiguration,
+			customInstructions,
 			diffEnabled,
-			task, 
+			debugDiffEnabled,
+			task,
 			images
 		)
 	}
 
 	async initClineWithHistoryItem(historyItem: HistoryItem) {
 		await this.clearTask()
-		const { 
-			apiConfiguration, 
-			customInstructions, 
+		const {
+			apiConfiguration,
+			customInstructions,
 			diffEnabled,
+			debugDiffEnabled,
 		} = await this.getState()
 		
 		this.cline = new Cline(
@@ -236,6 +240,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			apiConfiguration,
 			customInstructions,
 			diffEnabled,
+			debugDiffEnabled,
 			undefined,
 			undefined,
 			historyItem,
@@ -597,6 +602,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,
@@ -923,6 +933,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			alwaysAllowMcp,
 			soundEnabled,
 			diffEnabled,
+			debugDiffEnabled,
 			taskHistory,
 		} = await this.getState()
 		
@@ -946,6 +957,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,
 		}
@@ -1040,6 +1052,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			allowedCommands,
 			soundEnabled,
 			diffEnabled,
+			debugDiffEnabled,
 		] = await Promise.all([
 			this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
 			this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1077,6 +1090,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
 			this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
 			this.getGlobalState("diffEnabled") as Promise<boolean | undefined>,
+			this.getGlobalState("debugDiffEnabled") as Promise<boolean | undefined>,
 		])
 
 		let apiProvider: ApiProvider
@@ -1130,8 +1144,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			alwaysAllowMcp: alwaysAllowMcp ?? false,
 			taskHistory,
 			allowedCommands,
-			soundEnabled,
-			diffEnabled,
+			soundEnabled: soundEnabled ?? false,
+			diffEnabled: diffEnabled ?? false,
+			debugDiffEnabled: debugDiffEnabled ?? false,
 		}
 	}
 

+ 32 - 0
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);
+    });
+});

+ 4 - 7
src/integrations/misc/extract-text.ts

@@ -53,15 +53,12 @@ async function extractTextFromIPYNB(filePath: string): Promise<string> {
 
 	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')
-}
-
-
+}

+ 1 - 0
src/shared/ExtensionMessage.ts

@@ -52,6 +52,7 @@ export interface ExtensionState {
 	allowedCommands?: string[]
 	soundEnabled?: boolean
 	diffEnabled?: boolean
+	debugDiffEnabled?: boolean
 }
 
 export interface ClineMessage {

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -33,6 +33,7 @@ export interface WebviewMessage {
 		| "playSound"
 		| "soundEnabled"
 		| "diffEnabled"
+		| "debugDiffEnabled"
 		| "openMcpSettings"
 		| "restartMcpServer"
 		| "toggleToolAlwaysAllow"

+ 21 - 1
webview-ui/src/components/settings/SettingsView.tsx

@@ -31,6 +31,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 		setSoundEnabled,
 		diffEnabled,
 		setDiffEnabled,
+		debugDiffEnabled,
+		setDebugDiffEnabled,
 		openRouterModels,
 		setAllowedCommands,
 		allowedCommands,
@@ -46,7 +48,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 })
@@ -56,6 +61,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 			vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] })
 			vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
 			vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
+			vscode.postMessage({ type: "debugDiffEnabled", bool: debugDiffEnabled })
 			onDone()
 		}
 	}
@@ -324,6 +330,20 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 							When enabled, Cline will play sound effects for notifications and events.
 						</p>
 					</div>
+
+					<div style={{ marginBottom: 5 }}>
+						<VSCodeCheckbox checked={debugDiffEnabled} onChange={(e: any) => setDebugDiffEnabled(e.target.checked)}>
+							<span style={{ fontWeight: "500" }}>Debug diff operations</span>
+						</VSCodeCheckbox>
+						<p
+							style={{
+								fontSize: "12px",
+								marginTop: "5px",
+								color: "var(--vscode-descriptionForeground)",
+							}}>
+							When enabled, Cline will show detailed debug information when applying diffs fails.
+						</p>
+					</div>
 				</div>
 
 				{IS_DEV && (

+ 10 - 1
webview-ui/src/context/ExtensionStateContext.tsx

@@ -30,6 +30,7 @@ export interface ExtensionStateContextType extends ExtensionState {
 	setAllowedCommands: (value: string[]) => void
 	setSoundEnabled: (value: boolean) => void
 	setDiffEnabled: (value: boolean) => void
+	setDebugDiffEnabled: (value: boolean) => void
 }
 
 const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -43,6 +44,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		allowedCommands: [],
 		soundEnabled: false,
 		diffEnabled: false,
+		debugDiffEnabled: false,
 	})
 	const [didHydrateState, setDidHydrateState] = useState(false)
 	const [showWelcome, setShowWelcome] = useState(false)
@@ -129,7 +131,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		openRouterModels,
 		mcpServers,
 		filePaths,
-		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 })),
@@ -140,6 +145,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
 		setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
 		setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
+		setDebugDiffEnabled: (value) => setState((prevState) => ({
+			...prevState,
+			debugDiffEnabled: value
+		})),
 	}
 
 	return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>