From 0325eaa96f699a7506547e269e3e40a566135ddd Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Thu, 10 Jul 2025 15:50:55 -0600 Subject: [PATCH 01/31] feat: Add settings to control diagnostic messages (#5524) - Add includeDiagnosticMessages toggle setting - Add maxDiagnosticMessages slider setting (1-100) - Update diagnostic processing to respect settings - Add UI controls in Context Management settings - Update all 17 language translations - Add comprehensive unit tests - Fix default value consistency --- packages/types/src/global-settings.ts | 6 + src/core/mentions/index.ts | 12 +- .../mentions/processUserContentMentions.ts | 10 + src/core/task/Task.ts | 8 +- .../tools/__tests__/writeToFileTool.spec.ts | 1 + src/core/tools/applyDiffTool.ts | 8 + src/core/tools/insertContentTool.ts | 9 + src/core/tools/searchAndReplaceTool.ts | 9 + src/core/tools/writeToFileTool.ts | 8 + src/core/webview/ClineProvider.ts | 3 + src/core/webview/webviewMessageHandler.ts | 8 + .../diagnostics/__tests__/diagnostics.spec.ts | 228 +++++++++ src/integrations/diagnostics/index.ts | 173 +++++-- src/integrations/editor/DiffViewProvider.ts | 9 + src/shared/ExtensionMessage.ts | 2 + src/shared/WebviewMessage.ts | 2 + .../settings/ContextManagementSettings.tsx | 42 ++ .../src/components/settings/SettingsView.tsx | 6 + .../ContextManagementSettings.spec.tsx | 437 +++++------------- .../src/context/ExtensionStateContext.tsx | 14 + webview-ui/src/i18n/locales/ca/settings.json | 10 + webview-ui/src/i18n/locales/de/settings.json | 10 + webview-ui/src/i18n/locales/en/settings.json | 10 + webview-ui/src/i18n/locales/es/settings.json | 10 + webview-ui/src/i18n/locales/fr/settings.json | 10 + webview-ui/src/i18n/locales/hi/settings.json | 12 +- webview-ui/src/i18n/locales/id/settings.json | 10 + webview-ui/src/i18n/locales/it/settings.json | 10 + webview-ui/src/i18n/locales/ja/settings.json | 10 + webview-ui/src/i18n/locales/ko/settings.json | 10 + webview-ui/src/i18n/locales/nl/settings.json | 10 + webview-ui/src/i18n/locales/pl/settings.json | 10 + .../src/i18n/locales/pt-BR/settings.json | 10 + webview-ui/src/i18n/locales/ru/settings.json | 10 + webview-ui/src/i18n/locales/tr/settings.json | 10 + webview-ui/src/i18n/locales/vi/settings.json | 10 + .../src/i18n/locales/zh-CN/settings.json | 10 + .../src/i18n/locales/zh-TW/settings.json | 12 +- 38 files changed, 802 insertions(+), 377 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 906949919a5..495523bd45f 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -72,6 +72,9 @@ export const globalSettingsSchema = z.object({ autoCondenseContextPercent: z.number().optional(), maxConcurrentFileReads: z.number().optional(), + includeDiagnosticMessages: z.boolean().optional(), + maxDiagnosticMessages: z.number().optional(), + browserToolEnabled: z.boolean().optional(), browserViewportSize: z.string().optional(), screenshotQuality: z.number().optional(), @@ -261,6 +264,9 @@ export const EVALS_SETTINGS: RooCodeSettings = { showRooIgnoredFiles: true, maxReadFileLine: -1, // -1 to enable full file reading. + includeDiagnosticMessages: true, + maxDiagnosticMessages: 50, + language: "en", telemetrySetting: "enabled", diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 780b27d1f70..cb7ed349f2a 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -80,6 +80,8 @@ export async function parseMentions( fileContextTracker?: FileContextTracker, rooIgnoreController?: RooIgnoreController, showRooIgnoredFiles: boolean = true, + includeDiagnosticMessages: boolean = true, + maxDiagnosticMessages: number = 5, ): Promise { const mentions: Set = new Set() let parsedText = text.replace(mentionRegexGlobal, (match, mention) => { @@ -165,7 +167,7 @@ export async function parseMentions( } } else if (mention === "problems") { try { - const problems = await getWorkspaceProblems(cwd) + const problems = await getWorkspaceProblems(cwd, includeDiagnosticMessages, maxDiagnosticMessages) parsedText += `\n\n\n${problems}\n` } catch (error) { parsedText += `\n\n\nError fetching diagnostics: ${error.message}\n` @@ -286,12 +288,18 @@ async function getFileOrFolderContent( } } -async function getWorkspaceProblems(cwd: string): Promise { +async function getWorkspaceProblems( + cwd: string, + includeDiagnosticMessages: boolean = true, + maxDiagnosticMessages: number = 5, +): Promise { const diagnostics = vscode.languages.getDiagnostics() const result = await diagnosticsToProblemsString( diagnostics, [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning], cwd, + includeDiagnosticMessages, + maxDiagnosticMessages, ) if (!result) { return "No errors or warnings detected." diff --git a/src/core/mentions/processUserContentMentions.ts b/src/core/mentions/processUserContentMentions.ts index 3f131a1c05f..215f3434eb4 100644 --- a/src/core/mentions/processUserContentMentions.ts +++ b/src/core/mentions/processUserContentMentions.ts @@ -13,6 +13,8 @@ export async function processUserContentMentions({ fileContextTracker, rooIgnoreController, showRooIgnoredFiles = true, + includeDiagnosticMessages = true, + maxDiagnosticMessages = 5, }: { userContent: Anthropic.Messages.ContentBlockParam[] cwd: string @@ -20,6 +22,8 @@ export async function processUserContentMentions({ fileContextTracker: FileContextTracker rooIgnoreController?: any showRooIgnoredFiles?: boolean + includeDiagnosticMessages?: boolean + maxDiagnosticMessages?: number }) { // Process userContent array, which contains various block types: // TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam. @@ -46,6 +50,8 @@ export async function processUserContentMentions({ fileContextTracker, rooIgnoreController, showRooIgnoredFiles, + includeDiagnosticMessages, + maxDiagnosticMessages, ), } } @@ -63,6 +69,8 @@ export async function processUserContentMentions({ fileContextTracker, rooIgnoreController, showRooIgnoredFiles, + includeDiagnosticMessages, + maxDiagnosticMessages, ), } } @@ -81,6 +89,8 @@ export async function processUserContentMentions({ fileContextTracker, rooIgnoreController, showRooIgnoredFiles, + includeDiagnosticMessages, + maxDiagnosticMessages, ), } } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d4a5d823d51..8a3d9754fe0 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1225,7 +1225,11 @@ export class Task extends EventEmitter { }), ) - const { showRooIgnoredFiles = true } = (await this.providerRef.deref()?.getState()) ?? {} + const { + showRooIgnoredFiles = true, + includeDiagnosticMessages = true, + maxDiagnosticMessages = 50, + } = (await this.providerRef.deref()?.getState()) ?? {} const parsedUserContent = await processUserContentMentions({ userContent, @@ -1234,6 +1238,8 @@ export class Task extends EventEmitter { fileContextTracker: this.fileContextTracker, rooIgnoreController: this.rooIgnoreController, showRooIgnoredFiles, + includeDiagnosticMessages, + maxDiagnosticMessages, }) const environmentDetails = await getEnvironmentDetails(this, includeFileDetails) diff --git a/src/core/tools/__tests__/writeToFileTool.spec.ts b/src/core/tools/__tests__/writeToFileTool.spec.ts index 1b8582c9cc4..78e60cbaa58 100644 --- a/src/core/tools/__tests__/writeToFileTool.spec.ts +++ b/src/core/tools/__tests__/writeToFileTool.spec.ts @@ -157,6 +157,7 @@ describe("writeToFileTool", () => { finalContent: "final content", }), scrollToFirstDiff: vi.fn(), + updateDiagnosticSettings: vi.fn(), pushToolWriteResult: vi.fn().mockImplementation(async function ( this: any, task: any, diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts index ad4bb0590f8..24cc61830c5 100644 --- a/src/core/tools/applyDiffTool.ts +++ b/src/core/tools/applyDiffTool.ts @@ -142,6 +142,14 @@ export async function applyDiffToolLegacy( cline.consecutiveMistakeCount = 0 cline.consecutiveMistakeCountForApplyDiff.delete(relPath) + // Get diagnostic settings from state + const state = await cline.providerRef?.deref()?.getState() + const includeDiagnosticMessages = state?.includeDiagnosticMessages ?? true + const maxDiagnosticMessages = state?.maxDiagnosticMessages + + // Update DiffViewProvider with diagnostic settings + cline.diffViewProvider.updateDiagnosticSettings(includeDiagnosticMessages, maxDiagnosticMessages) + // Show diff view before asking for approval cline.diffViewProvider.editType = "modify" await cline.diffViewProvider.open(relPath) diff --git a/src/core/tools/insertContentTool.ts b/src/core/tools/insertContentTool.ts index 2b312244006..8ffc62daa2d 100644 --- a/src/core/tools/insertContentTool.ts +++ b/src/core/tools/insertContentTool.ts @@ -98,6 +98,15 @@ export async function insertContentTool( cline.diffViewProvider.editType = fileExists ? "modify" : "create" cline.diffViewProvider.originalContent = fileContent + + // Update diagnostic settings from global state + const state = await cline.providerRef?.deref()?.getState() + if (state) { + cline.diffViewProvider.updateDiagnosticSettings( + state.includeDiagnosticMessages ?? true, + state.maxDiagnosticMessages ?? 5, + ) + } const lines = fileExists ? fileContent.split("\n") : [] const updatedContent = insertGroups(lines, [ diff --git a/src/core/tools/searchAndReplaceTool.ts b/src/core/tools/searchAndReplaceTool.ts index b6ec3ed39b0..f0c1e9838d3 100644 --- a/src/core/tools/searchAndReplaceTool.ts +++ b/src/core/tools/searchAndReplaceTool.ts @@ -191,6 +191,15 @@ export async function searchAndReplaceTool( cline.diffViewProvider.editType = "modify" cline.diffViewProvider.originalContent = fileContent + // Update diagnostic settings from global state + const state = await cline.providerRef?.deref()?.getState() + if (state) { + cline.diffViewProvider.updateDiagnosticSettings( + state.includeDiagnosticMessages ?? true, + state.maxDiagnosticMessages ?? 5, + ) + } + // Generate and validate diff const diff = formatResponse.createPrettyPatch(validRelPath, fileContent, newContent) if (!diff) { diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index fd9d158f3f7..fd5295d80b2 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -97,6 +97,14 @@ export async function writeToFileTool( isProtected: isWriteProtected, } + // Get diagnostic settings from state + const state = await cline.providerRef?.deref()?.getState() + const includeDiagnosticMessages = state?.includeDiagnosticMessages ?? true + const maxDiagnosticMessages = state?.maxDiagnosticMessages + + // Update DiffViewProvider with diagnostic settings + cline.diffViewProvider.updateDiagnosticSettings(includeDiagnosticMessages, maxDiagnosticMessages) + try { if (block.partial) { // update gui message diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 96d42ec3d1d..de10b20c105 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1726,6 +1726,9 @@ export class ClineProvider codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore, }, profileThresholds: stateValues.profileThresholds ?? {}, + // Add diagnostic message settings + includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true, + maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 5, } } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 780d40df891..6c9c343595b 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1254,6 +1254,14 @@ export const webviewMessageHandler = async ( await updateGlobalState("maxConcurrentFileReads", valueToSave) await provider.postStateToWebview() break + case "includeDiagnosticMessages": + await updateGlobalState("includeDiagnosticMessages", message.bool ?? true) + await provider.postStateToWebview() + break + case "maxDiagnosticMessages": + await updateGlobalState("maxDiagnosticMessages", message.value ?? 100) + await provider.postStateToWebview() + break case "setHistoryPreviewCollapsed": // Add the new case handler await updateGlobalState("historyPreviewCollapsed", message.bool ?? false) // No need to call postStateToWebview here as the UI already updated optimistically diff --git a/src/integrations/diagnostics/__tests__/diagnostics.spec.ts b/src/integrations/diagnostics/__tests__/diagnostics.spec.ts index 2ce3e0ada8d..e40f7060265 100644 --- a/src/integrations/diagnostics/__tests__/diagnostics.spec.ts +++ b/src/integrations/diagnostics/__tests__/diagnostics.spec.ts @@ -383,4 +383,232 @@ describe("diagnosticsToProblemsString", () => { expect(vscode.workspace.fs.stat).toHaveBeenCalledWith(fileUri) expect(vscode.workspace.openTextDocument).toHaveBeenCalledWith(fileUri) }) + it("should return empty string when includeDiagnostics is false", async () => { + // Mock file URI + const fileUri = vscode.Uri.file("/path/to/file.ts") + + // Create diagnostics + const diagnostics = [ + new vscode.Diagnostic(new vscode.Range(0, 0, 0, 10), "Error message", vscode.DiagnosticSeverity.Error), + new vscode.Diagnostic(new vscode.Range(1, 0, 1, 10), "Warning message", vscode.DiagnosticSeverity.Warning), + ] + + // Mock fs.stat to return file type + const mockStat = { + type: vscode.FileType.File, + } + vscode.workspace.fs.stat = vitest.fn().mockResolvedValue(mockStat) + + // Mock document content + const mockDocument = { + lineAt: vitest.fn((line) => ({ + text: `Line ${line + 1} content`, + })), + } + vscode.workspace.openTextDocument = vitest.fn().mockResolvedValue(mockDocument) + + // Test with includeDiagnostics set to false + const result = await diagnosticsToProblemsString( + [[fileUri, diagnostics]], + [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning], + "/path/to", + false, // includeDiagnostics + ) + + // Verify empty string is returned + expect(result).toBe("") + + // Verify no file operations were performed + expect(vscode.workspace.fs.stat).not.toHaveBeenCalled() + expect(vscode.workspace.openTextDocument).not.toHaveBeenCalled() + }) + + it("should limit diagnostics when maxDiagnostics is specified", async () => { + // Mock file URI + const fileUri = vscode.Uri.file("/path/to/file.ts") + + // Create multiple diagnostics + const diagnostics = [ + new vscode.Diagnostic(new vscode.Range(0, 0, 0, 10), "Error 1", vscode.DiagnosticSeverity.Error), + new vscode.Diagnostic(new vscode.Range(1, 0, 1, 10), "Warning 1", vscode.DiagnosticSeverity.Warning), + new vscode.Diagnostic(new vscode.Range(2, 0, 2, 10), "Error 2", vscode.DiagnosticSeverity.Error), + new vscode.Diagnostic(new vscode.Range(3, 0, 3, 10), "Warning 2", vscode.DiagnosticSeverity.Warning), + new vscode.Diagnostic(new vscode.Range(4, 0, 4, 10), "Error 3", vscode.DiagnosticSeverity.Error), + ] + + // Mock fs.stat to return file type + const mockStat = { + type: vscode.FileType.File, + } + vscode.workspace.fs.stat = vitest.fn().mockResolvedValue(mockStat) + + // Mock document content + const mockDocument = { + lineAt: vitest.fn((line) => ({ + text: `Line ${line + 1} content`, + })), + } + vscode.workspace.openTextDocument = vitest.fn().mockResolvedValue(mockDocument) + + // Test with maxDiagnostics set to 3 + const result = await diagnosticsToProblemsString( + [[fileUri, diagnostics]], + [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning], + "/path/to", + true, // includeDiagnostics + 3, // maxDiagnostics + ) + + // Verify only 3 diagnostics are included (prioritizing errors) + expect(result).toContain("Error 1") + expect(result).toContain("Error 2") + expect(result).toContain("Error 3") + expect(result).not.toContain("Warning 1") + expect(result).not.toContain("Warning 2") + + // Verify the limit message is included + expect(result).toContain("(Showing 3 of 5 total diagnostics)") + }) + + it("should prioritize errors over warnings when limiting diagnostics", async () => { + // Mock file URIs + const fileUri1 = vscode.Uri.file("/path/to/file1.ts") + const fileUri2 = vscode.Uri.file("/path/to/file2.ts") + + // Create diagnostics with mixed severities + const diagnostics1 = [ + new vscode.Diagnostic(new vscode.Range(0, 0, 0, 10), "Warning in file1", vscode.DiagnosticSeverity.Warning), + new vscode.Diagnostic(new vscode.Range(1, 0, 1, 10), "Error in file1", vscode.DiagnosticSeverity.Error), + ] + + const diagnostics2 = [ + new vscode.Diagnostic(new vscode.Range(0, 0, 0, 10), "Error in file2", vscode.DiagnosticSeverity.Error), + new vscode.Diagnostic(new vscode.Range(1, 0, 1, 10), "Warning in file2", vscode.DiagnosticSeverity.Warning), + new vscode.Diagnostic( + new vscode.Range(2, 0, 2, 10), + "Info in file2", + vscode.DiagnosticSeverity.Information, + ), + ] + + // Mock fs.stat to return file type + const mockStat = { + type: vscode.FileType.File, + } + vscode.workspace.fs.stat = vitest.fn().mockResolvedValue(mockStat) + + // Mock document content + const mockDocument = { + lineAt: vitest.fn((line) => ({ + text: `Line ${line + 1} content`, + })), + } + vscode.workspace.openTextDocument = vitest.fn().mockResolvedValue(mockDocument) + + // Test with maxDiagnostics set to 2 + const result = await diagnosticsToProblemsString( + [ + [fileUri1, diagnostics1], + [fileUri2, diagnostics2], + ], + [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning, vscode.DiagnosticSeverity.Information], + "/path/to", + true, // includeDiagnostics + 2, // maxDiagnostics + ) + + // Verify only errors are included (2 errors total) + expect(result).toContain("Error in file1") + expect(result).toContain("Error in file2") + expect(result).not.toContain("Warning in file1") + expect(result).not.toContain("Warning in file2") + expect(result).not.toContain("Info in file2") + + // Verify the limit message is included + expect(result).toContain("(Showing 2 of 5 total diagnostics)") + }) + + it("should handle maxDiagnostics with no limit when undefined", async () => { + // Mock file URI + const fileUri = vscode.Uri.file("/path/to/file.ts") + + // Create multiple diagnostics + const diagnostics = [ + new vscode.Diagnostic(new vscode.Range(0, 0, 0, 10), "Error 1", vscode.DiagnosticSeverity.Error), + new vscode.Diagnostic(new vscode.Range(1, 0, 1, 10), "Warning 1", vscode.DiagnosticSeverity.Warning), + new vscode.Diagnostic(new vscode.Range(2, 0, 2, 10), "Error 2", vscode.DiagnosticSeverity.Error), + ] + + // Mock fs.stat to return file type + const mockStat = { + type: vscode.FileType.File, + } + vscode.workspace.fs.stat = vitest.fn().mockResolvedValue(mockStat) + + // Mock document content + const mockDocument = { + lineAt: vitest.fn((line) => ({ + text: `Line ${line + 1} content`, + })), + } + vscode.workspace.openTextDocument = vitest.fn().mockResolvedValue(mockDocument) + + // Test with maxDiagnostics undefined + const result = await diagnosticsToProblemsString( + [[fileUri, diagnostics]], + [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning], + "/path/to", + true, // includeDiagnostics + undefined, // maxDiagnostics + ) + + // Verify all diagnostics are included + expect(result).toContain("Error 1") + expect(result).toContain("Warning 1") + expect(result).toContain("Error 2") + + // Verify no limit message is included + expect(result).not.toContain("(Showing") + }) + + it("should handle maxDiagnostics of 0 as no limit", async () => { + // Mock file URI + const fileUri = vscode.Uri.file("/path/to/file.ts") + + // Create multiple diagnostics + const diagnostics = [ + new vscode.Diagnostic(new vscode.Range(0, 0, 0, 10), "Error 1", vscode.DiagnosticSeverity.Error), + new vscode.Diagnostic(new vscode.Range(1, 0, 1, 10), "Warning 1", vscode.DiagnosticSeverity.Warning), + ] + + // Mock fs.stat to return file type + const mockStat = { + type: vscode.FileType.File, + } + vscode.workspace.fs.stat = vitest.fn().mockResolvedValue(mockStat) + + // Mock document content + const mockDocument = { + lineAt: vitest.fn((line) => ({ + text: `Line ${line + 1} content`, + })), + } + vscode.workspace.openTextDocument = vitest.fn().mockResolvedValue(mockDocument) + + // Test with maxDiagnostics set to 0 + const result = await diagnosticsToProblemsString( + [[fileUri, diagnostics]], + [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning], + "/path/to", + true, // includeDiagnostics + 0, // maxDiagnostics (should be treated as no limit) + ) + + // Verify all diagnostics are included + expect(result).toContain("Error 1") + expect(result).toContain("Warning 1") + + // Verify no limit message is included + expect(result).not.toContain("(Showing") + }) }) diff --git a/src/integrations/diagnostics/index.ts b/src/integrations/diagnostics/index.ts index 97b83353534..bc09bf3d31e 100644 --- a/src/integrations/diagnostics/index.ts +++ b/src/integrations/diagnostics/index.ts @@ -74,55 +74,150 @@ export async function diagnosticsToProblemsString( diagnostics: [vscode.Uri, vscode.Diagnostic[]][], severities: vscode.DiagnosticSeverity[], cwd: string, + includeDiagnostics: boolean = true, + maxDiagnostics?: number, ): Promise { + // If diagnostics are disabled, return empty string + if (!includeDiagnostics) { + return "" + } + const documents = new Map() const fileStats = new Map() let result = "" - for (const [uri, fileDiagnostics] of diagnostics) { - const problems = fileDiagnostics - .filter((d) => severities.includes(d.severity)) - .sort((a, b) => a.range.start.line - b.range.start.line) - if (problems.length > 0) { - result += `\n\n${path.relative(cwd, uri.fsPath).toPosix()}` - for (const diagnostic of problems) { - let label: string - switch (diagnostic.severity) { - case vscode.DiagnosticSeverity.Error: - label = "Error" - break - case vscode.DiagnosticSeverity.Warning: - label = "Warning" - break - case vscode.DiagnosticSeverity.Information: - label = "Information" - break - case vscode.DiagnosticSeverity.Hint: - label = "Hint" - break - default: - label = "Diagnostic" + let diagnosticCount = 0 + + // If we have a limit, we need to collect all diagnostics first, sort by severity, then limit + if (maxDiagnostics && maxDiagnostics > 0) { + // Flatten all diagnostics with their URIs + const allDiagnostics: { uri: vscode.Uri; diagnostic: vscode.Diagnostic }[] = [] + for (const [uri, fileDiagnostics] of diagnostics) { + const filtered = fileDiagnostics.filter((d) => severities.includes(d.severity)) + for (const diagnostic of filtered) { + allDiagnostics.push({ uri, diagnostic }) + } + } + + // Sort by severity (errors first) and then by line number + allDiagnostics.sort((a, b) => { + const severityDiff = a.diagnostic.severity - b.diagnostic.severity + if (severityDiff !== 0) return severityDiff + return a.diagnostic.range.start.line - b.diagnostic.range.start.line + }) + + // Take only the first maxDiagnostics + const limitedDiagnostics = allDiagnostics.slice(0, maxDiagnostics) + + // Group back by URI for processing + const groupedDiagnostics = new Map() + for (const { uri, diagnostic } of limitedDiagnostics) { + const key = uri.toString() + if (!groupedDiagnostics.has(key)) { + groupedDiagnostics.set(key, { uri, diagnostics: [] }) + } + groupedDiagnostics.get(key)!.diagnostics.push(diagnostic) + } + + // Process the limited diagnostics + for (const { uri, diagnostics: fileDiagnostics } of groupedDiagnostics.values()) { + const problems = fileDiagnostics.sort((a, b) => a.range.start.line - b.range.start.line) + if (problems.length > 0) { + result += `\n\n${path.relative(cwd, uri.fsPath).toPosix()}` + for (const diagnostic of problems) { + let label: string + switch (diagnostic.severity) { + case vscode.DiagnosticSeverity.Error: + label = "Error" + break + case vscode.DiagnosticSeverity.Warning: + label = "Warning" + break + case vscode.DiagnosticSeverity.Information: + label = "Information" + break + case vscode.DiagnosticSeverity.Hint: + label = "Hint" + break + default: + label = "Diagnostic" + } + const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed + const source = diagnostic.source ? `${diagnostic.source} ` : "" + try { + let fileStat = fileStats.get(uri) + if (!fileStat) { + fileStat = await vscode.workspace.fs.stat(uri) + fileStats.set(uri, fileStat) + } + if (fileStat.type === vscode.FileType.File) { + const document = documents.get(uri) || (await vscode.workspace.openTextDocument(uri)) + documents.set(uri, document) + const lineContent = document.lineAt(diagnostic.range.start.line).text + result += `\n- [${source}${label}] ${line} | ${lineContent} : ${diagnostic.message}` + } else { + result += `\n- [${source}${label}] 1 | (directory) : ${diagnostic.message}` + } + } catch { + result += `\n- [${source}${label}] ${line} | (unavailable) : ${diagnostic.message}` + } } - const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed - const source = diagnostic.source ? `${diagnostic.source} ` : "" - try { - let fileStat = fileStats.get(uri) - if (!fileStat) { - fileStat = await vscode.workspace.fs.stat(uri) - fileStats.set(uri, fileStat) + } + } + + // Add a note if we hit the limit + if (allDiagnostics.length > maxDiagnostics) { + result += `\n\n(Showing ${maxDiagnostics} of ${allDiagnostics.length} total diagnostics)` + } + } else { + // No limit, process all diagnostics as before + for (const [uri, fileDiagnostics] of diagnostics) { + const problems = fileDiagnostics + .filter((d) => severities.includes(d.severity)) + .sort((a, b) => a.range.start.line - b.range.start.line) + if (problems.length > 0) { + result += `\n\n${path.relative(cwd, uri.fsPath).toPosix()}` + for (const diagnostic of problems) { + let label: string + switch (diagnostic.severity) { + case vscode.DiagnosticSeverity.Error: + label = "Error" + break + case vscode.DiagnosticSeverity.Warning: + label = "Warning" + break + case vscode.DiagnosticSeverity.Information: + label = "Information" + break + case vscode.DiagnosticSeverity.Hint: + label = "Hint" + break + default: + label = "Diagnostic" } - if (fileStat.type === vscode.FileType.File) { - const document = documents.get(uri) || (await vscode.workspace.openTextDocument(uri)) - documents.set(uri, document) - const lineContent = document.lineAt(diagnostic.range.start.line).text - result += `\n- [${source}${label}] ${line} | ${lineContent} : ${diagnostic.message}` - } else { - result += `\n- [${source}${label}] 1 | (directory) : ${diagnostic.message}` + const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed + const source = diagnostic.source ? `${diagnostic.source} ` : "" + try { + let fileStat = fileStats.get(uri) + if (!fileStat) { + fileStat = await vscode.workspace.fs.stat(uri) + fileStats.set(uri, fileStat) + } + if (fileStat.type === vscode.FileType.File) { + const document = documents.get(uri) || (await vscode.workspace.openTextDocument(uri)) + documents.set(uri, document) + const lineContent = document.lineAt(diagnostic.range.start.line).text + result += `\n- [${source}${label}] ${line} | ${lineContent} : ${diagnostic.message}` + } else { + result += `\n- [${source}${label}] 1 | (directory) : ${diagnostic.message}` + } + } catch { + result += `\n- [${source}${label}] ${line} | (unavailable) : ${diagnostic.message}` } - } catch { - result += `\n- [${source}${label}] ${line} | (unavailable) : ${diagnostic.message}` + diagnosticCount++ } } } } + return result.trim() } diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index f4133029c99..03b52fb1128 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -36,9 +36,16 @@ export class DiffViewProvider { private activeLineController?: DecorationController private streamedLines: string[] = [] private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [] + private includeDiagnosticMessages: boolean = true + private maxDiagnosticMessages?: number constructor(private cwd: string) {} + updateDiagnosticSettings(includeDiagnosticMessages: boolean, maxDiagnosticMessages?: number): void { + this.includeDiagnosticMessages = includeDiagnosticMessages + this.maxDiagnosticMessages = maxDiagnosticMessages + } + async open(relPath: string): Promise { this.relPath = relPath const fileExists = this.editType === "modify" @@ -240,6 +247,8 @@ export class DiffViewProvider { vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention) ], this.cwd, + this.includeDiagnosticMessages, + this.maxDiagnosticMessages, ) // Will be empty string if no errors. newProblemsMessage = diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 4f2aa2da159..7b3fa787fd7 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -234,6 +234,8 @@ export type ExtensionState = Pick< | "codebaseIndexConfig" | "codebaseIndexModels" | "profileThresholds" + | "includeDiagnosticMessages" + | "maxDiagnosticMessages" > & { version: string clineMessages: ClineMessage[] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 1f56829f7b3..53b4fa92a7e 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -162,6 +162,8 @@ export interface WebviewMessage { | "language" | "maxReadFileLine" | "maxConcurrentFileReads" + | "includeDiagnosticMessages" + | "maxDiagnosticMessages" | "searchFiles" | "toggleApiConfigPin" | "setHistoryPreviewCollapsed" diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 4afcc6f7b4e..6d0cefb2559 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -22,6 +22,8 @@ type ContextManagementSettingsProps = HTMLAttributes & { maxReadFileLine?: number maxConcurrentFileReads?: number profileThresholds?: Record + includeDiagnosticMessages?: boolean + maxDiagnosticMessages?: number setCachedStateField: SetCachedStateField< | "autoCondenseContext" | "autoCondenseContextPercent" @@ -31,6 +33,8 @@ type ContextManagementSettingsProps = HTMLAttributes & { | "maxReadFileLine" | "maxConcurrentFileReads" | "profileThresholds" + | "includeDiagnosticMessages" + | "maxDiagnosticMessages" > } @@ -45,6 +49,8 @@ export const ContextManagementSettings = ({ maxReadFileLine, maxConcurrentFileReads, profileThresholds = {}, + includeDiagnosticMessages, + maxDiagnosticMessages, className, ...props }: ContextManagementSettingsProps) => { @@ -196,6 +202,42 @@ export const ContextManagementSettings = ({ {t("settings:contextManagement.maxReadFile.description")} + +
+ setCachedStateField("includeDiagnosticMessages", e.target.checked)} + data-testid="include-diagnostic-messages-checkbox"> + + +
+ {t("settings:contextManagement.diagnostics.includeMessages.description")} +
+
+ + {includeDiagnosticMessages && ( +
+ + {t("settings:contextManagement.diagnostics.maxMessages.label")} + +
+ setCachedStateField("maxDiagnosticMessages", value)} + data-testid="max-diagnostic-messages-slider" + /> + {maxDiagnosticMessages ?? 50} +
+
+ {t("settings:contextManagement.diagnostics.maxMessages.description")} +
+
+ )}
(({ onDone, t alwaysAllowFollowupQuestions, alwaysAllowUpdateTodoList, followupAutoApproveTimeoutMs, + includeDiagnosticMessages, + maxDiagnosticMessages, } = cachedState const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) @@ -320,6 +322,8 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles }) vscode.postMessage({ type: "maxReadFileLine", value: maxReadFileLine ?? -1 }) vscode.postMessage({ type: "maxConcurrentFileReads", value: cachedState.maxConcurrentFileReads ?? 5 }) + vscode.postMessage({ type: "includeDiagnosticMessages", bool: includeDiagnosticMessages }) + vscode.postMessage({ type: "maxDiagnosticMessages", value: maxDiagnosticMessages ?? 50 }) vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName }) vscode.postMessage({ type: "updateExperimental", values: experiments }) vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch }) @@ -666,6 +670,8 @@ const SettingsView = forwardRef(({ onDone, t maxReadFileLine={maxReadFileLine} maxConcurrentFileReads={maxConcurrentFileReads} profileThresholds={profileThresholds} + includeDiagnosticMessages={includeDiagnosticMessages} + maxDiagnosticMessages={maxDiagnosticMessages} setCachedStateField={setCachedStateField} /> )} diff --git a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx index 5db05970522..778fdfb51a6 100644 --- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx @@ -1,399 +1,170 @@ -import { render, screen, fireEvent } from "@/utils/test-utils" - -import { ContextManagementSettings } from "@src/components/settings/ContextManagementSettings" - -// Mock translation hook to return the key as the translation -vitest.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ - t: (key: string) => key, - }), +// npx vitest src/components/settings/__tests__/ContextManagementSettings.spec.tsx + +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" +import { ContextManagementSettings } from "../ContextManagementSettings" + +// Mock the UI components +vi.mock("@/components/ui", () => ({ + ...vi.importActual("@/components/ui"), + Slider: ({ value, onValueChange, "data-testid": dataTestId, disabled }: any) => ( + onValueChange([parseFloat(e.target.value)])} + data-testid={dataTestId} + disabled={disabled} + role="slider" + /> + ), + Input: ({ value, onChange, "data-testid": dataTestId, ...props }: any) => ( + + ), + Button: ({ children, onClick, ...props }: any) => ( + + ), + Select: ({ children, ...props }: any) =>
{children}
, + SelectTrigger: ({ children, ...props }: any) =>
{children}
, + SelectValue: ({ children, ...props }: any) =>
{children}
, + SelectContent: ({ children, ...props }: any) =>
{children}
, + SelectItem: ({ children, ...props }: any) =>
{children}
, })) // Mock vscode utilities - this is necessary since we're not in a VSCode environment -vitest.mock("@/utils/vscode", () => ({ +vi.mock("@/utils/vscode", () => ({ vscode: { - postMessage: vitest.fn(), + postMessage: vi.fn(), }, })) // Mock VSCode components to behave like standard HTML elements -vitest.mock("@vscode/webview-ui-toolkit/react", () => ({ +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ VSCodeCheckbox: ({ checked, onChange, children, "data-testid": dataTestId, ...props }: any) => ( -
+
- ), - VSCodeTextArea: ({ value, onChange, rows, className, ...props }: any) => ( -