Просмотр исходного кода

Send only new errors to claude after he applies an edit

Saoud Rizwan 1 год назад
Родитель
Сommit
f282df604a
3 измененных файлов с 129 добавлено и 25 удалено
  1. 32 2
      src/ClaudeDev.ts
  2. 7 23
      src/utils/context-mentions.ts
  3. 90 0
      src/utils/diagnostics.ts

+ 32 - 2
src/ClaudeDev.ts

@@ -28,6 +28,7 @@ import { extractTextFromFile } from "./utils/extract-text"
 import { regexSearchFiles } from "./utils/ripgrep"
 import { parseMentions } from "./utils/context-mentions"
 import { UrlContentFetcher } from "./utils/UrlContentFetcher"
+import { diagnosticsToProblemsString, getNewDiagnostics } from "./utils/diagnostics"
 
 const SYSTEM_PROMPT =
 	async () => `You are Claude Dev, a highly skilled software developer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
@@ -762,6 +763,9 @@ export class ClaudeDev {
 				}
 			}
 
+			// get diagnostics before editing the file, we'll compare to diagnostics after editing to see if claude needs to fix anything
+			const preDiagnostics = vscode.languages.getDiagnostics()
+
 			let originalContent: string
 			if (fileExists) {
 				originalContent = await fs.readFile(absolutePath, "utf-8")
@@ -1037,6 +1041,27 @@ export class ClaudeDev {
 
 			await this.closeDiffViews()
 
+			/*
+			Getting diagnostics before and after the file edit is a better approach than
+			automatically tracking problems in real-time. This method ensures we only
+			report new problems that are a direct result of this specific edit.
+			Since these are new problems resulting from Claude's edit, we know they're
+			directly related to the work he's doing. This eliminates the risk of Claude
+			going off-task or getting distracted by unrelated issues, which was a problem
+			with the previous auto-debug approach. Some users' machines may be slow to
+			update diagnostics, so this approach provides a good balance between automation
+			and avoiding potential issues where Claude might get stuck in loops due to
+			outdated problem information. If no new problems show up by the time the user
+			accepts the changes, they can always debug later using the '@problems' mention.
+			This way, Claude only becomes aware of new problems resulting from his edits
+			and can address them accordingly. If problems don't change immediately after
+			applying a fix, Claude won't be notified, which is generally fine since the
+			initial fix is usually correct and it may just take time for linters to catch up.
+			*/
+			const postDiagnostics = vscode.languages.getDiagnostics()
+			const newProblems = diagnosticsToProblemsString(getNewDiagnostics(preDiagnostics, postDiagnostics), cwd) // will be empty string if no errors/warnings
+			const newProblemsMessage =
+				newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
 			// await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
 
 			// If the edited content has different EOL characters, we don't want to show a diff with all the EOL differences.
@@ -1056,11 +1081,16 @@ export class ClaudeDev {
 				return [
 					false,
 					await this.formatToolResult(
-						`The user made the following updates to your content:\n\n${userDiff}\n\nThe updated content, which includes both your original modifications and the user's additional edits, has been successfully saved to ${relPath}. Note this does not mean you need to re-write the file with the user's changes, they have already been applied to the file.`
+						`The user made the following updates to your content:\n\n${userDiff}\n\nThe updated content, which includes both your original modifications and the user's additional edits, has been successfully saved to ${relPath}. (Note this does not mean you need to re-write the file with the user's changes, as they have already been applied to the file.)${newProblemsMessage}`
 					),
 				]
 			} else {
-				return [false, await this.formatToolResult(`The content was successfully saved to ${relPath}.`)]
+				return [
+					false,
+					await this.formatToolResult(
+						`The content was successfully saved to ${relPath}.${newProblemsMessage}`
+					),
+				]
 			}
 		} catch (error) {
 			const errorString = `Error writing file: ${JSON.stringify(serializeError(error))}`

+ 7 - 23
src/utils/context-mentions.ts

@@ -6,6 +6,7 @@ import { mentionRegexGlobal } from "../shared/context-mentions"
 import fs from "fs/promises"
 import { extractTextFromFile } from "./extract-text"
 import { isBinaryFile } from "isbinaryfile"
+import { diagnosticsToProblemsString } from "./diagnostics"
 
 export function openMention(mention?: string): void {
 	if (!mention) {
@@ -93,8 +94,8 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
 			}
 		} else if (mention === "problems") {
 			try {
-				const diagnostics = await getWorkspaceDiagnostics(cwd)
-				parsedText += `\n\n<workspace_diagnostics>\n${diagnostics}\n</workspace_diagnostics>`
+				const problems = getWorkspaceProblems(cwd)
+				parsedText += `\n\n<workspace_diagnostics>\n${problems}\n</workspace_diagnostics>`
 			} catch (error) {
 				parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
 			}
@@ -168,28 +169,11 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
 	}
 }
 
-async function getWorkspaceDiagnostics(cwd: string): Promise<string> {
+function getWorkspaceProblems(cwd: string): string {
 	const diagnostics = vscode.languages.getDiagnostics()
-
-	let diagnosticsDetails = ""
-	for (const [uri, fileDiagnostics] of diagnostics) {
-		const problems = fileDiagnostics.filter(
-			(d) => d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning
-		)
-		if (problems.length > 0) {
-			diagnosticsDetails += `\nFile: ${path.relative(cwd, uri.fsPath)}`
-			for (const diagnostic of problems) {
-				let severity = diagnostic.severity === vscode.DiagnosticSeverity.Error ? "Error" : "Warning"
-				const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
-				const source = diagnostic.source ? `${diagnostic.source} ` : ""
-				diagnosticsDetails += `\n- [${source}${severity}] Line ${line}: ${diagnostic.message}`
-			}
-		}
-	}
-
-	if (!diagnosticsDetails) {
+	const result = diagnosticsToProblemsString(diagnostics, cwd)
+	if (!result) {
 		return "No errors or warnings detected."
 	}
-
-	return diagnosticsDetails.trim()
+	return result
 }

+ 90 - 0
src/utils/diagnostics.ts

@@ -0,0 +1,90 @@
+import * as vscode from "vscode"
+import * as path from "path"
+import deepEqual from "fast-deep-equal"
+
+export function getNewDiagnostics(
+	oldDiagnostics: [vscode.Uri, vscode.Diagnostic[]][],
+	newDiagnostics: [vscode.Uri, vscode.Diagnostic[]][]
+): [vscode.Uri, vscode.Diagnostic[]][] {
+	const newProblems: [vscode.Uri, vscode.Diagnostic[]][] = []
+	const oldMap = new Map(oldDiagnostics)
+
+	for (const [uri, newDiags] of newDiagnostics) {
+		const oldDiags = oldMap.get(uri) || []
+		const newProblemsForUri = newDiags.filter((newDiag) => !oldDiags.some((oldDiag) => deepEqual(oldDiag, newDiag)))
+
+		if (newProblemsForUri.length > 0) {
+			newProblems.push([uri, newProblemsForUri])
+		}
+	}
+
+	return newProblems
+}
+
+// Usage:
+// const oldDiagnostics = // ... your old diagnostics array
+// const newDiagnostics = // ... your new diagnostics array
+// const newProblems = getNewDiagnostics(oldDiagnostics, newDiagnostics);
+
+// Example usage with mocks:
+//
+// // Mock old diagnostics
+// const oldDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [
+//     [vscode.Uri.file("/path/to/file1.ts"), [
+//         new vscode.Diagnostic(new vscode.Range(0, 0, 0, 10), "Old error in file1", vscode.DiagnosticSeverity.Error)
+//     ]],
+//     [vscode.Uri.file("/path/to/file2.ts"), [
+//         new vscode.Diagnostic(new vscode.Range(5, 5, 5, 15), "Old warning in file2", vscode.DiagnosticSeverity.Warning)
+//     ]]
+// ];
+//
+// // Mock new diagnostics
+// const newDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [
+//     [vscode.Uri.file("/path/to/file1.ts"), [
+//         new vscode.Diagnostic(new vscode.Range(0, 0, 0, 10), "Old error in file1", vscode.DiagnosticSeverity.Error),
+//         new vscode.Diagnostic(new vscode.Range(2, 2, 2, 12), "New error in file1", vscode.DiagnosticSeverity.Error)
+//     ]],
+//     [vscode.Uri.file("/path/to/file2.ts"), [
+//         new vscode.Diagnostic(new vscode.Range(5, 5, 5, 15), "Old warning in file2", vscode.DiagnosticSeverity.Warning)
+//     ]],
+//     [vscode.Uri.file("/path/to/file3.ts"), [
+//         new vscode.Diagnostic(new vscode.Range(1, 1, 1, 11), "New error in file3", vscode.DiagnosticSeverity.Error)
+//     ]]
+// ];
+//
+// const newProblems = getNewProblems(oldDiagnostics, newDiagnostics);
+//
+// console.log("New problems:");
+// for (const [uri, diagnostics] of newProblems) {
+//     console.log(`File: ${uri.fsPath}`);
+//     for (const diagnostic of diagnostics) {
+//         console.log(`- ${diagnostic.message} (${diagnostic.range.start.line}:${diagnostic.range.start.character})`);
+//     }
+// }
+//
+// // Expected output:
+// // New problems:
+// // File: /path/to/file1.ts
+// // - New error in file1 (2:2)
+// // File: /path/to/file3.ts
+// // - New error in file3 (1:1)
+
+// will return empty string if no errors/warnings
+export function diagnosticsToProblemsString(diagnostics: [vscode.Uri, vscode.Diagnostic[]][], cwd: string): string {
+	let result = ""
+	for (const [uri, fileDiagnostics] of diagnostics) {
+		const problems = fileDiagnostics.filter(
+			(d) => d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning
+		)
+		if (problems.length > 0) {
+			result += `\n\n${path.relative(cwd, uri.fsPath)}`
+			for (const diagnostic of problems) {
+				let severity = diagnostic.severity === vscode.DiagnosticSeverity.Error ? "Error" : "Warning"
+				const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
+				const source = diagnostic.source ? `${diagnostic.source} ` : ""
+				result += `\n- [${source}${severity}] Line ${line}: ${diagnostic.message}`
+			}
+		}
+	}
+	return result.trim()
+}