diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 906949919a5..2754086dba2 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -72,6 +72,17 @@ export const globalSettingsSchema = z.object({ autoCondenseContextPercent: z.number().optional(), maxConcurrentFileReads: z.number().optional(), + /** + * Whether to include diagnostic messages (errors, warnings) in tool outputs + * @default true + */ + includeDiagnosticMessages: z.boolean().optional(), + /** + * Maximum number of diagnostic messages to include in tool outputs + * @default 50 + */ + maxDiagnosticMessages: z.number().optional(), + browserToolEnabled: z.boolean().optional(), browserViewportSize: z.string().optional(), screenshotQuality: z.number().optional(), @@ -261,6 +272,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..7ce54b984e1 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 = 50, ): 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 = 50, +): 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..0649c4bc3c3 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 = 50, }: { 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..d12c0a2ffe9 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -260,7 +260,7 @@ export class Task extends EventEmitter { this.consecutiveMistakeLimit = consecutiveMistakeLimit ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT this.providerRef = new WeakRef(provider) this.globalStoragePath = provider.context.globalStorageUri.fsPath - this.diffViewProvider = new DiffViewProvider(this.cwd) + this.diffViewProvider = new DiffViewProvider(this.cwd, this) this.enableCheckpoints = enableCheckpoints this.rootTask = rootTask @@ -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__/insertContentTool.spec.ts b/src/core/tools/__tests__/insertContentTool.spec.ts index e23d7aaa33c..5f055fb29a4 100644 --- a/src/core/tools/__tests__/insertContentTool.spec.ts +++ b/src/core/tools/__tests__/insertContentTool.spec.ts @@ -96,6 +96,7 @@ describe("insertContentTool", () => { 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/__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/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 96d42ec3d1d..d7b5d0c9a89 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1439,7 +1439,8 @@ export class ClineProvider profileThresholds, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs, - diagnosticsEnabled, + includeDiagnosticMessages, + maxDiagnosticMessages, } = await this.getState() const telemetryKey = process.env.POSTHOG_API_KEY @@ -1560,7 +1561,8 @@ export class ClineProvider hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false, alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, - diagnosticsEnabled: diagnosticsEnabled ?? true, + includeDiagnosticMessages: includeDiagnosticMessages ?? true, + maxDiagnosticMessages: maxDiagnosticMessages ?? 50, } } @@ -1726,6 +1728,9 @@ export class ClineProvider codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore, }, profileThresholds: stateValues.profileThresholds ?? {}, + // Add diagnostic message settings + includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true, + maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50, } } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 780d40df891..87db74c666f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1254,6 +1254,16 @@ export const webviewMessageHandler = async ( await updateGlobalState("maxConcurrentFileReads", valueToSave) await provider.postStateToWebview() break + case "includeDiagnosticMessages": + // Only apply default if the value is truly undefined (not false) + const includeValue = message.bool !== undefined ? message.bool : true + await updateGlobalState("includeDiagnosticMessages", includeValue) + await provider.postStateToWebview() + break + case "maxDiagnosticMessages": + await updateGlobalState("maxDiagnosticMessages", message.value ?? 50) + 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..24116ad9905 100644 --- a/src/integrations/diagnostics/__tests__/diagnostics.spec.ts +++ b/src/integrations/diagnostics/__tests__/diagnostics.spec.ts @@ -383,4 +383,234 @@ 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 based on count when maxDiagnostics is specified", async () => { + // Mock file URI + const fileUri = vscode.Uri.file("/path/to/file.ts") + + // Create multiple diagnostics with varying message lengths + 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 (should include exactly 3 diagnostics) + const result = await diagnosticsToProblemsString( + [[fileUri, diagnostics]], + [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning], + "/path/to", + true, // includeDiagnostics + 3, // maxDiagnostics (count limit) + ) + + // Verify that exactly 3 diagnostics are included, prioritizing errors + expect(result).toContain("Error 1") + expect(result).toContain("Error 2") + expect(result).toContain("Error 3") + // Warnings should not be included since we have 3 errors and limit is 3 + expect(result).not.toContain("Warning 1") + expect(result).not.toContain("Warning 2") + + // Verify the limit message is included + expect(result).toContain("2 more problems omitted to prevent context overflow") + }) + + it("should prioritize errors over warnings when limiting diagnostics by count", 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 (should include exactly 2 diagnostics) + const result = await diagnosticsToProblemsString( + [ + [fileUri1, diagnostics1], + [fileUri2, diagnostics2], + ], + [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning, vscode.DiagnosticSeverity.Information], + "/path/to", + true, // includeDiagnostics + 2, // maxDiagnostics (count limit) + ) + + // Verify exactly 2 errors are included (prioritized over warnings) + expect(result).toContain("Error in file1") + expect(result).toContain("Error in file2") + // Warnings and info should not be included + 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("3 more problems omitted to prevent context overflow") + }) + + 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..5fdba435253 100644 --- a/src/integrations/diagnostics/index.ts +++ b/src/integrations/diagnostics/index.ts @@ -74,55 +74,170 @@ export async function diagnosticsToProblemsString( diagnostics: [vscode.Uri, vscode.Diagnostic[]][], severities: vscode.DiagnosticSeverity[], cwd: string, + includeDiagnosticMessages: boolean = true, + maxDiagnosticMessages?: number, ): Promise { + // If diagnostics are disabled, return empty string + if (!includeDiagnosticMessages) { + 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" + + // If we have a limit, use count-based limiting + if (maxDiagnosticMessages && maxDiagnosticMessages > 0) { + let includedCount = 0 + let totalCount = 0 + + // Flatten all diagnostics with their URIs + const allDiagnostics: { uri: vscode.Uri; diagnostic: vscode.Diagnostic; formattedText?: string }[] = [] + for (const [uri, fileDiagnostics] of diagnostics) { + const filtered = fileDiagnostics.filter((d) => severities.includes(d.severity)) + for (const diagnostic of filtered) { + allDiagnostics.push({ uri, diagnostic }) + totalCount++ + } + } + + // 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 + }) + + // Process diagnostics up to the count limit + const includedDiagnostics: typeof allDiagnostics = [] + for (const item of allDiagnostics) { + // Stop if we've reached the count limit + if (includedCount >= maxDiagnosticMessages) { + break + } + + // Format the diagnostic + let label: string + switch (item.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 = item.diagnostic.range.start.line + 1 + const source = item.diagnostic.source ? `${item.diagnostic.source} ` : "" + + // Pre-format the diagnostic text + let diagnosticText = "" + try { + let fileStat = fileStats.get(item.uri) + if (!fileStat) { + fileStat = await vscode.workspace.fs.stat(item.uri) + fileStats.set(item.uri, fileStat) + } + if (fileStat.type === vscode.FileType.File) { + const document = documents.get(item.uri) || (await vscode.workspace.openTextDocument(item.uri)) + documents.set(item.uri, document) + const lineContent = document.lineAt(item.diagnostic.range.start.line).text + diagnosticText = `\n- [${source}${label}] ${line} | ${lineContent} : ${item.diagnostic.message}` + } else { + diagnosticText = `\n- [${source}${label}] 1 | (directory) : ${item.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) + } catch { + diagnosticText = `\n- [${source}${label}] ${line} | (unavailable) : ${item.diagnostic.message}` + } + + item.formattedText = diagnosticText + includedDiagnostics.push(item) + includedCount++ + } + + // Group included diagnostics by URI for output + const groupedDiagnostics = new Map() + for (const item of includedDiagnostics) { + const key = item.uri.toString() + if (!groupedDiagnostics.has(key)) { + groupedDiagnostics.set(key, { uri: item.uri, diagnostics: [] }) + } + groupedDiagnostics.get(key)!.diagnostics.push(item) + } + + // Build the output + for (const { uri, diagnostics: fileDiagnostics } of groupedDiagnostics.values()) { + const sortedDiagnostics = fileDiagnostics.sort( + (a, b) => a.diagnostic.range.start.line - b.diagnostic.range.start.line, + ) + if (sortedDiagnostics.length > 0) { + result += `\n\n${path.relative(cwd, uri.fsPath).toPosix()}` + for (const item of sortedDiagnostics) { + result += item.formattedText + } + } + } + + // Add a note if we hit the limit + if (totalCount > includedCount) { + result += `\n\n... ${totalCount - includedCount} more problems omitted to prevent context overflow` + } + } 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}` } } } } + return result.trim() } diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index f4133029c99..64820beffb4 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -36,8 +36,14 @@ export class DiffViewProvider { private activeLineController?: DecorationController private streamedLines: string[] = [] private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [] + private taskRef: WeakRef - constructor(private cwd: string) {} + constructor( + private cwd: string, + task: Task, + ) { + this.taskRef = new WeakRef(task) + } async open(relPath: string): Promise { this.relPath = relPath @@ -234,12 +240,20 @@ export class DiffViewProvider { const postDiagnostics = vscode.languages.getDiagnostics() + // Get diagnostic settings from state + const task = this.taskRef.deref() + const state = await task?.providerRef.deref()?.getState() + const includeDiagnosticMessages = state?.includeDiagnosticMessages ?? true + const maxDiagnosticMessages = state?.maxDiagnosticMessages ?? 50 + const newProblems = await diagnosticsToProblemsString( getNewDiagnostics(this.preDiagnostics, postDiagnostics), [ 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, + includeDiagnosticMessages, + maxDiagnosticMessages, ) // Will be empty string if no errors. newProblemsMessage = diff --git a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts index a4aded95bb9..7159aca57a2 100644 --- a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts +++ b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts @@ -101,6 +101,7 @@ describe("DiffViewProvider", () => { let diffViewProvider: DiffViewProvider const mockCwd = "/mock/cwd" let mockWorkspaceEdit: { replace: any; delete: any } + let mockTask: any beforeEach(() => { vi.clearAllMocks() @@ -110,7 +111,19 @@ describe("DiffViewProvider", () => { } vi.mocked(vscode.WorkspaceEdit).mockImplementation(() => mockWorkspaceEdit as any) - diffViewProvider = new DiffViewProvider(mockCwd) + // Create a mock Task instance + mockTask = { + providerRef: { + deref: vi.fn().mockReturnValue({ + getState: vi.fn().mockResolvedValue({ + includeDiagnosticMessages: true, + maxDiagnosticMessages: 50, + }), + }), + }, + } + + diffViewProvider = new DiffViewProvider(mockCwd, mockTask) // Mock the necessary properties and methods ;(diffViewProvider as any).relPath = "test.txt" ;(diffViewProvider as any).activeDiffEditor = { 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/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index e1d3c52cb93..1a93fee01a4 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -20,7 +20,6 @@ type AutoApproveSettingsProps = HTMLAttributes & { alwaysAllowWrite?: boolean alwaysAllowWriteOutsideWorkspace?: boolean alwaysAllowWriteProtected?: boolean - writeDelayMs: number alwaysAllowBrowser?: boolean alwaysApproveResubmit?: boolean requestDelaySeconds: number @@ -39,7 +38,6 @@ type AutoApproveSettingsProps = HTMLAttributes & { | "alwaysAllowWrite" | "alwaysAllowWriteOutsideWorkspace" | "alwaysAllowWriteProtected" - | "writeDelayMs" | "alwaysAllowBrowser" | "alwaysApproveResubmit" | "requestDelaySeconds" @@ -61,7 +59,6 @@ export const AutoApproveSettings = ({ alwaysAllowWrite, alwaysAllowWriteOutsideWorkspace, alwaysAllowWriteProtected, - writeDelayMs, alwaysAllowBrowser, alwaysApproveResubmit, requestDelaySeconds, @@ -216,22 +213,6 @@ export const AutoApproveSettings = ({ {t("settings:autoApprove.write.protected.description")} -
-
- setCachedStateField("writeDelayMs", value)} - data-testid="write-delay-slider" - /> - {writeDelayMs}ms -
-
- {t("settings:autoApprove.write.delayLabel")} -
-
)} diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 4afcc6f7b4e..4530fdb1ba1 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -5,7 +5,7 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { Database, FoldVertical } from "lucide-react" import { cn } from "@/lib/utils" -import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider } from "@/components/ui" +import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider, Button } from "@/components/ui" import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" @@ -22,6 +22,9 @@ type ContextManagementSettingsProps = HTMLAttributes & { maxReadFileLine?: number maxConcurrentFileReads?: number profileThresholds?: Record + includeDiagnosticMessages?: boolean + maxDiagnosticMessages?: number + writeDelayMs: number setCachedStateField: SetCachedStateField< | "autoCondenseContext" | "autoCondenseContextPercent" @@ -31,6 +34,9 @@ type ContextManagementSettingsProps = HTMLAttributes & { | "maxReadFileLine" | "maxConcurrentFileReads" | "profileThresholds" + | "includeDiagnosticMessages" + | "maxDiagnosticMessages" + | "writeDelayMs" > } @@ -45,6 +51,9 @@ export const ContextManagementSettings = ({ maxReadFileLine, maxConcurrentFileReads, profileThresholds = {}, + includeDiagnosticMessages, + maxDiagnosticMessages, + writeDelayMs, className, ...props }: ContextManagementSettingsProps) => { @@ -196,6 +205,95 @@ 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")} +
+
+ +
+ + {t("settings:contextManagement.diagnostics.maxMessages.label")} + +
+ { + // When slider reaches 100, set to -1 (unlimited) + setCachedStateField("maxDiagnosticMessages", value === 100 ? -1 : value) + }} + data-testid="max-diagnostic-messages-slider" + aria-label={t("settings:contextManagement.diagnostics.maxMessages.label")} + aria-valuemin={1} + aria-valuemax={100} + aria-valuenow={ + maxDiagnosticMessages !== undefined && maxDiagnosticMessages <= 0 + ? 100 + : (maxDiagnosticMessages ?? 50) + } + aria-valuetext={ + (maxDiagnosticMessages !== undefined && maxDiagnosticMessages <= 0) || + maxDiagnosticMessages === 100 + ? t("settings:contextManagement.diagnostics.maxMessages.unlimitedLabel") + : `${maxDiagnosticMessages ?? 50} ${t("settings:contextManagement.diagnostics.maxMessages.label")}` + } + /> + + {(maxDiagnosticMessages !== undefined && maxDiagnosticMessages <= 0) || + maxDiagnosticMessages === 100 + ? t("settings:contextManagement.diagnostics.maxMessages.unlimitedLabel") + : (maxDiagnosticMessages ?? 50)} + + +
+
+ {t("settings:contextManagement.diagnostics.maxMessages.description")} +
+
+ +
+ + {t("settings:contextManagement.diagnostics.delayAfterWrite.label")} + +
+ setCachedStateField("writeDelayMs", value)} + data-testid="write-delay-slider" + /> + {writeDelayMs}ms +
+
+ {t("settings:contextManagement.diagnostics.delayAfterWrite.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 }) @@ -606,7 +610,6 @@ const SettingsView = forwardRef(({ onDone, t alwaysAllowWrite={alwaysAllowWrite} alwaysAllowWriteOutsideWorkspace={alwaysAllowWriteOutsideWorkspace} alwaysAllowWriteProtected={alwaysAllowWriteProtected} - writeDelayMs={writeDelayMs} alwaysAllowBrowser={alwaysAllowBrowser} alwaysApproveResubmit={alwaysApproveResubmit} requestDelaySeconds={requestDelaySeconds} @@ -666,6 +669,9 @@ const SettingsView = forwardRef(({ onDone, t maxReadFileLine={maxReadFileLine} maxConcurrentFileReads={maxConcurrentFileReads} profileThresholds={profileThresholds} + includeDiagnosticMessages={includeDiagnosticMessages} + maxDiagnosticMessages={maxDiagnosticMessages} + writeDelayMs={writeDelayMs} 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..61444267f21 100644 --- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx @@ -1,122 +1,320 @@ -import { render, screen, fireEvent } from "@/utils/test-utils" +// npx vitest src/components/settings/__tests__/ContextManagementSettings.spec.tsx -import { ContextManagementSettings } from "@src/components/settings/ContextManagementSettings" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" +import { ContextManagementSettings } from "../ContextManagementSettings" -// Mock translation hook to return the key as the translation -vitest.mock("@/i18n/TranslationContext", () => ({ +// Mock the translation hook +vi.mock("@/hooks/useAppTranslation", () => ({ useAppTranslation: () => ({ - t: (key: string) => key, + t: (key: string) => { + // Return specific translations for our test cases + if (key === "settings:contextManagement.diagnostics.maxMessages.unlimitedLabel") { + return "Unlimited" + } + return key + }, }), })) +// 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)])} + onKeyDown={(e) => { + const currentValue = value?.[0] ?? 0 + if (e.key === "ArrowRight") { + onValueChange([currentValue + 1]) + } else if (e.key === "ArrowLeft") { + onValueChange([currentValue - 1]) + } + }} + 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) => ( -