diff --git a/src/ClaudeDev.ts b/src/ClaudeDev.ts index 027b261..6ff3afa 100644 --- a/src/ClaudeDev.ts +++ b/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))}` diff --git a/src/utils/context-mentions.ts b/src/utils/context-mentions.ts index 833510e..4203e5c 100644 --- a/src/utils/context-mentions.ts +++ b/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\n${diagnostics}\n` + const problems = getWorkspaceProblems(cwd) + parsedText += `\n\n\n${problems}\n` } catch (error) { parsedText += `\n\n\nError fetching diagnostics: ${error.message}\n` } @@ -168,28 +169,11 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise } } -async function getWorkspaceDiagnostics(cwd: string): Promise { +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 } diff --git a/src/utils/diagnostics.ts b/src/utils/diagnostics.ts new file mode 100644 index 0000000..29129c9 --- /dev/null +++ b/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() +}