Send only new errors to claude after he applies an edit

This commit is contained in:
Saoud Rizwan
2024-09-20 16:23:43 -04:00
parent 25831b5752
commit f282df604a
3 changed files with 129 additions and 25 deletions

View File

@@ -28,6 +28,7 @@ import { extractTextFromFile } from "./utils/extract-text"
import { regexSearchFiles } from "./utils/ripgrep" import { regexSearchFiles } from "./utils/ripgrep"
import { parseMentions } from "./utils/context-mentions" import { parseMentions } from "./utils/context-mentions"
import { UrlContentFetcher } from "./utils/UrlContentFetcher" import { UrlContentFetcher } from "./utils/UrlContentFetcher"
import { diagnosticsToProblemsString, getNewDiagnostics } from "./utils/diagnostics"
const SYSTEM_PROMPT = 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. 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 let originalContent: string
if (fileExists) { if (fileExists) {
originalContent = await fs.readFile(absolutePath, "utf-8") originalContent = await fs.readFile(absolutePath, "utf-8")
@@ -1037,6 +1041,27 @@ export class ClaudeDev {
await this.closeDiffViews() 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 }) // 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. // 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 [ return [
false, false,
await this.formatToolResult( 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 { } 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) { } catch (error) {
const errorString = `Error writing file: ${JSON.stringify(serializeError(error))}` const errorString = `Error writing file: ${JSON.stringify(serializeError(error))}`

View File

@@ -6,6 +6,7 @@ import { mentionRegexGlobal } from "../shared/context-mentions"
import fs from "fs/promises" import fs from "fs/promises"
import { extractTextFromFile } from "./extract-text" import { extractTextFromFile } from "./extract-text"
import { isBinaryFile } from "isbinaryfile" import { isBinaryFile } from "isbinaryfile"
import { diagnosticsToProblemsString } from "./diagnostics"
export function openMention(mention?: string): void { export function openMention(mention?: string): void {
if (!mention) { if (!mention) {
@@ -93,8 +94,8 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
} }
} else if (mention === "problems") { } else if (mention === "problems") {
try { try {
const diagnostics = await getWorkspaceDiagnostics(cwd) const problems = getWorkspaceProblems(cwd)
parsedText += `\n\n<workspace_diagnostics>\n${diagnostics}\n</workspace_diagnostics>` parsedText += `\n\n<workspace_diagnostics>\n${problems}\n</workspace_diagnostics>`
} catch (error) { } catch (error) {
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>` 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() const diagnostics = vscode.languages.getDiagnostics()
const result = diagnosticsToProblemsString(diagnostics, cwd)
let diagnosticsDetails = "" if (!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) {
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) {
return "No errors or warnings detected." return "No errors or warnings detected."
} }
return result
return diagnosticsDetails.trim()
} }

90
src/utils/diagnostics.ts Normal file
View File

@@ -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()
}