From 6ab504a36f3bf4f0088db549b569ec7ba0d73304 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Sat, 14 Jun 2025 18:41:16 -0600 Subject: [PATCH 01/10] fix: add diagnostics settings to global state (#2379) - Add includeDiagnostics, maxDiagnosticsCount, and diagnosticsFilter to global settings - Create DiagnosticsSettings component for UI configuration - Update ClineProvider to handle diagnostics settings messages - Modify diagnosticsToProblemsString to accept options parameter - Update ExtensionState type and related components - Add proper defaults: includeDiagnostics=false, maxDiagnosticsCount=5, diagnosticsFilter=['error','warning'] This allows users to configure diagnostics settings that persist across sessions instead of being reset to defaults on each restart. --- packages/types/src/global-settings.ts | 4 + src/core/mentions/index.ts | 8 ++ src/core/webview/ClineProvider.ts | 9 ++ .../webview/__tests__/ClineProvider.test.ts | 3 + src/core/webview/webviewMessageHandler.ts | 12 ++ src/integrations/diagnostics/index.ts | 47 ++++++++ src/package.json | 19 ++++ src/package.nls.json | 5 +- src/shared/ExtensionMessage.ts | 7 ++ src/shared/WebviewMessage.ts | 3 + .../settings/DiagnosticsSettings.tsx | 105 ++++++++++++++++++ .../src/context/ExtensionStateContext.tsx | 12 ++ .../__tests__/ExtensionStateContext.test.tsx | 3 + 13 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 webview-ui/src/components/settings/DiagnosticsSettings.tsx diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 5b729a125f..d3444ae333 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -70,6 +70,10 @@ export const globalSettingsSchema = z.object({ showRooIgnoredFiles: z.boolean().optional(), maxReadFileLine: z.number().optional(), + includeDiagnostics: z.boolean().optional(), + maxDiagnosticsCount: z.number().optional(), + diagnosticsFilter: z.array(z.string()).optional(), + terminalOutputLineLimit: z.number().optional(), terminalShellIntegrationTimeout: z.number().optional(), terminalShellIntegrationDisabled: z.boolean().optional(), diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 8ae4f7f131..601cf4a41f 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -246,6 +246,14 @@ async function getFileOrFolderContent( } async function getWorkspaceProblems(cwd: string): Promise { + // Check if diagnostics are enabled + const config = vscode.workspace.getConfiguration("roo-cline") + const includeDiagnostics = config.get("includeDiagnostics", true) + + if (!includeDiagnostics) { + return "Diagnostics are disabled in settings." + } + const diagnostics = vscode.languages.getDiagnostics() const result = await diagnosticsToProblemsString( diagnostics, diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 57fa16a848..51f0d2f90d 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1329,6 +1329,9 @@ export class ClineProvider showRooIgnoredFiles, language, maxReadFileLine, + includeDiagnostics, + maxDiagnosticsCount, + diagnosticsFilter, terminalCompressProgressBar, historyPreviewCollapsed, cloudUserInfo, @@ -1439,6 +1442,9 @@ export class ClineProvider renderContext: this.renderContext, maxReadFileLine: maxReadFileLine ?? -1, maxConcurrentFileReads: maxConcurrentFileReads ?? 5, + includeDiagnostics: includeDiagnostics ?? false, + maxDiagnosticsCount: maxDiagnosticsCount ?? 5, + diagnosticsFilter: diagnosticsFilter ?? ["error", "warning"], settingsImportedAt: this.settingsImportedAt, terminalCompressProgressBar: terminalCompressProgressBar ?? true, hasSystemPromptOverride, @@ -1590,6 +1596,9 @@ export class ClineProvider showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true, maxReadFileLine: stateValues.maxReadFileLine ?? -1, maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5, + includeDiagnostics: stateValues.includeDiagnostics ?? false, + maxDiagnosticsCount: stateValues.maxDiagnosticsCount ?? 5, + diagnosticsFilter: stateValues.diagnosticsFilter ?? ["error", "warning"], historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, cloudUserInfo, cloudIsAuthenticated, diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 6ced4989a4..b5144ca034 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -432,6 +432,9 @@ describe("ClineProvider", () => { autoCondenseContextPercent: 100, cloudIsAuthenticated: false, sharingEnabled: false, + includeDiagnostics: false, + maxDiagnosticsCount: 5, + diagnosticsFilter: ["error", "warning"], } const message: ExtensionMessage = { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a4d9dafecf..148397baef 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -995,6 +995,18 @@ export const webviewMessageHandler = async ( await updateGlobalState("maxConcurrentFileReads", valueToSave) await provider.postStateToWebview() break + case "includeDiagnostics": + await updateGlobalState("includeDiagnostics", message.bool ?? false) + await provider.postStateToWebview() + break + case "maxDiagnosticsCount": + await updateGlobalState("maxDiagnosticsCount", message.value ?? 5) + await provider.postStateToWebview() + break + case "diagnosticsFilter": + await updateGlobalState("diagnosticsFilter", message.values ?? ["error", "warning"]) + 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/index.ts b/src/integrations/diagnostics/index.ts index 97b8335353..0765f630a6 100644 --- a/src/integrations/diagnostics/index.ts +++ b/src/integrations/diagnostics/index.ts @@ -74,17 +74,62 @@ export async function diagnosticsToProblemsString( diagnostics: [vscode.Uri, vscode.Diagnostic[]][], severities: vscode.DiagnosticSeverity[], cwd: string, + options?: { + includeDiagnostics?: boolean + maxDiagnosticsCount?: number + diagnosticsFilter?: string[] + }, ): Promise { + // Use provided options or fall back to VSCode configuration + const includeDiagnostics = + options?.includeDiagnostics ?? + vscode.workspace.getConfiguration("roo-cline").get("includeDiagnostics", false) + + if (!includeDiagnostics) { + return "" + } + + const maxDiagnosticsCount = + options?.maxDiagnosticsCount ?? + vscode.workspace.getConfiguration("roo-cline").get("maxDiagnosticsCount", 5) + const diagnosticsFilter = + options?.diagnosticsFilter ?? + vscode.workspace.getConfiguration("roo-cline").get("diagnosticsFilter", ["error", "warning"]) + const documents = new Map() const fileStats = new Map() let result = "" + let totalDiagnosticsCount = 0 + for (const [uri, fileDiagnostics] of diagnostics) { const problems = fileDiagnostics .filter((d) => severities.includes(d.severity)) + .filter((d) => { + // Apply diagnostics filter + if (diagnosticsFilter.length === 0) return true + + const source = d.source || "" + const code = typeof d.code === "object" ? d.code.value : d.code + const filterKey = source ? `${source} ${code || ""}`.trim() : `${code || ""}`.trim() + + // Check if this diagnostic should be filtered out + return !diagnosticsFilter.some((filter) => { + // Support partial matching + return filterKey.includes(filter) || d.message.includes(filter) + }) + }) .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) { + // Check if we've reached the max count + if (maxDiagnosticsCount > 0 && totalDiagnosticsCount >= maxDiagnosticsCount) { + result += `\n... (${diagnostics.reduce((sum, [, diags]) => sum + diags.filter((d) => severities.includes(d.severity)).length, 0) - totalDiagnosticsCount} more diagnostics omitted)` + return result.trim() + } + let label: string switch (diagnostic.severity) { case vscode.DiagnosticSeverity.Error: @@ -121,6 +166,8 @@ export async function diagnosticsToProblemsString( } catch { result += `\n- [${source}${label}] ${line} | (unavailable) : ${diagnostic.message}` } + + totalDiagnosticsCount++ } } } diff --git a/src/package.json b/src/package.json index a806f66ffe..77ce1a1240 100644 --- a/src/package.json +++ b/src/package.json @@ -344,6 +344,25 @@ "type": "boolean", "default": false, "description": "%settings.rooCodeCloudEnabled.description%" + }, + "roo-cline.includeDiagnostics": { + "type": "boolean", + "default": true, + "description": "%settings.includeDiagnostics.description%" + }, + "roo-cline.maxDiagnosticsCount": { + "type": "number", + "default": 50, + "minimum": 0, + "description": "%settings.maxDiagnosticsCount.description%" + }, + "roo-cline.diagnosticsFilter": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "%settings.diagnosticsFilter.description%" } } } diff --git a/src/package.nls.json b/src/package.nls.json index b05dac3b36..4dd7f9ffd3 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -30,5 +30,8 @@ "settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)", "settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)", "settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Enable Roo Code Cloud." + "settings.rooCodeCloudEnabled.description": "Enable Roo Code Cloud.", + "settings.includeDiagnostics.description": "Include diagnostics (errors/warnings) in API requests. Disable this when creating multi-file features to avoid temporary errors distracting the AI.", + "settings.maxDiagnosticsCount.description": "Maximum number of diagnostics to include in API requests (0 for unlimited). Helps control token usage.", + "settings.diagnosticsFilter.description": "Filter diagnostics by code or source. Add diagnostic codes (e.g., 'dart Error', 'eslint/no-unused-vars') to exclude specific types." } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index ac19ba0ef2..95b0bb4451 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -177,6 +177,9 @@ export type ExtensionState = Pick< // | "showRooIgnoredFiles" // Optional in GlobalSettings, required here. // | "maxReadFileLine" // Optional in GlobalSettings, required here. | "maxConcurrentFileReads" // Optional in GlobalSettings, required here. + | "includeDiagnostics" + | "maxDiagnosticsCount" + | "diagnosticsFilter" | "terminalOutputLineLimit" | "terminalShellIntegrationTimeout" | "terminalShellIntegrationDisabled" @@ -223,6 +226,10 @@ export type ExtensionState = Pick< showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings maxReadFileLine: number // Maximum number of lines to read from a file before truncating + includeDiagnostics: boolean // Whether to include diagnostics in context + maxDiagnosticsCount: number // Maximum number of diagnostics to include + diagnosticsFilter: string[] // Filter for diagnostic severities + experiments: Experiments // Map of experiment IDs to their enabled state mcpEnabled: boolean diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 5186c716b9..c3477a2124 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -144,6 +144,9 @@ export interface WebviewMessage { | "language" | "maxReadFileLine" | "maxConcurrentFileReads" + | "includeDiagnostics" + | "maxDiagnosticsCount" + | "diagnosticsFilter" | "searchFiles" | "toggleApiConfigPin" | "setHistoryPreviewCollapsed" diff --git a/webview-ui/src/components/settings/DiagnosticsSettings.tsx b/webview-ui/src/components/settings/DiagnosticsSettings.tsx new file mode 100644 index 0000000000..bfe3f2173f --- /dev/null +++ b/webview-ui/src/components/settings/DiagnosticsSettings.tsx @@ -0,0 +1,105 @@ +import { HTMLAttributes } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { AlertCircle } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Input, Slider } from "@/components/ui" + +import { SetCachedStateField } from "./types" +import { SectionHeader } from "./SectionHeader" +import { Section } from "./Section" + +type DiagnosticsSettingsProps = HTMLAttributes & { + includeDiagnostics?: boolean + maxDiagnosticsCount?: number + diagnosticsFilter?: string[] + setCachedStateField: SetCachedStateField<"includeDiagnostics" | "maxDiagnosticsCount" | "diagnosticsFilter"> +} + +export const DiagnosticsSettings = ({ + includeDiagnostics, + maxDiagnosticsCount, + diagnosticsFilter, + setCachedStateField, + className, + ...props +}: DiagnosticsSettingsProps) => { + const { t } = useAppTranslation() + + const handleFilterChange = (value: string) => { + const filters = value + .split(",") + .map((f) => f.trim()) + .filter((f) => f.length > 0) + setCachedStateField("diagnosticsFilter", filters) + } + + return ( +
+ +
+ +
{t("settings:sections.diagnostics")}
+
+
+ +
+
+ setCachedStateField("includeDiagnostics", e.target.checked)} + data-testid="include-diagnostics-checkbox"> + + +
+ {t("settings:diagnostics.includeDiagnostics.description")} +
+
+ + {includeDiagnostics && ( +
+
+ + {t("settings:diagnostics.maxDiagnosticsCount.label")} + +
+ setCachedStateField("maxDiagnosticsCount", value)} + data-testid="max-diagnostics-count-slider" + /> + {maxDiagnosticsCount ?? 50} +
+
+ {t("settings:diagnostics.maxDiagnosticsCount.description")} +
+
+ +
+ + {t("settings:diagnostics.diagnosticsFilter.label")} + + handleFilterChange(e.target.value)} + placeholder="e.g., dart Error, eslint/no-unused-vars" + data-testid="diagnostics-filter-input" + /> +
+ {t("settings:diagnostics.diagnosticsFilter.description")} +
+
+
+ )} +
+
+ ) +} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index ab79f63df8..d8fdc9b1c4 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -118,6 +118,12 @@ export interface ExtensionStateContextType extends ExtensionState { autoCondenseContextPercent: number setAutoCondenseContextPercent: (value: number) => void routerModels?: RouterModels + includeDiagnostics: boolean + setIncludeDiagnostics: (value: boolean) => void + maxDiagnosticsCount: number + setMaxDiagnosticsCount: (value: number) => void + diagnosticsFilter: string[] + setDiagnosticsFilter: (value: string[]) => void } export const ExtensionStateContext = createContext(undefined) @@ -206,6 +212,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode codebaseIndexEmbedderModelId: "", }, codebaseIndexModels: { ollama: {}, openai: {} }, + includeDiagnostics: false, + maxDiagnosticsCount: 5, + diagnosticsFilter: ["error", "warning"], }) const [didHydrateState, setDidHydrateState] = useState(false) @@ -403,6 +412,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setCondensingApiConfigId: (value) => setState((prevState) => ({ ...prevState, condensingApiConfigId: value })), setCustomCondensingPrompt: (value) => setState((prevState) => ({ ...prevState, customCondensingPrompt: value })), + setIncludeDiagnostics: (value) => setState((prevState) => ({ ...prevState, includeDiagnostics: value })), + setMaxDiagnosticsCount: (value) => setState((prevState) => ({ ...prevState, maxDiagnosticsCount: value })), + setDiagnosticsFilter: (value) => setState((prevState) => ({ ...prevState, diagnosticsFilter: value })), } return {children} diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx index b8a6cadf98..7e9fab8dad 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx @@ -209,6 +209,9 @@ describe("mergeExtensionState", () => { autoCondenseContextPercent: 100, cloudIsAuthenticated: false, sharingEnabled: false, + includeDiagnostics: false, + maxDiagnosticsCount: 5, + diagnosticsFilter: ["error", "warning"], } const prevState: ExtensionState = { From 8117308183c7cb26866bb2168bbea13631c9417f Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Sat, 14 Jun 2025 18:42:31 -0600 Subject: [PATCH 02/10] fix: cast message.values to string[] for diagnosticsFilter --- src/core/webview/webviewMessageHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 148397baef..2a6eb6cddd 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1004,7 +1004,7 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() break case "diagnosticsFilter": - await updateGlobalState("diagnosticsFilter", message.values ?? ["error", "warning"]) + await updateGlobalState("diagnosticsFilter", (message.values as string[]) ?? ["error", "warning"]) await provider.postStateToWebview() break case "setHistoryPreviewCollapsed": // Add the new case handler From a46783ffbeacf31221015fd44a9b667ec8328feb Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Mon, 16 Jun 2025 09:47:09 -0600 Subject: [PATCH 03/10] fix: address PR #4712 review feedback for diagnostics settings - Added DiagnosticsSettings component to SettingsView with proper integration - Added missing localization keys for diagnostics section - Standardized includeDiagnostics default value to false across all files - Fixed double configuration reading in diagnosticsToProblemsString - Updated filter logic to use exact matching for diagnostic codes - Replaced any types with proper React event types - Added UI validation for max diagnostics count (0-200 range) - Added comprehensive test coverage for new functionality - Bumped version to 3.20.4 --- src/core/mentions/index.ts | 2 +- src/core/webview/ClineProvider.ts | 8 +- .../diagnosticsSettings.integration.test.ts | 180 ++++++++++++++++ .../diagnosticsToProblemsString.test.ts | 198 ++++++++++++++++++ src/integrations/diagnostics/index.ts | 21 +- src/package.json | 4 +- .../settings/DiagnosticsSettings.tsx | 12 +- .../src/components/settings/SettingsView.tsx | 19 ++ .../__tests__/DiagnosticsSettings.test.tsx | 182 ++++++++++++++++ webview-ui/src/i18n/locales/en/settings.json | 16 ++ 10 files changed, 619 insertions(+), 23 deletions(-) create mode 100644 src/core/webview/__tests__/diagnosticsSettings.integration.test.ts create mode 100644 src/integrations/diagnostics/__tests__/diagnosticsToProblemsString.test.ts create mode 100644 webview-ui/src/components/settings/__tests__/DiagnosticsSettings.test.tsx diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 601cf4a41f..2820d2c2fb 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -248,7 +248,7 @@ async function getFileOrFolderContent( async function getWorkspaceProblems(cwd: string): Promise { // Check if diagnostics are enabled const config = vscode.workspace.getConfiguration("roo-cline") - const includeDiagnostics = config.get("includeDiagnostics", true) + const includeDiagnostics = config.get("includeDiagnostics", false) if (!includeDiagnostics) { return "Diagnostics are disabled in settings." diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 51f0d2f90d..240a611786 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1443,8 +1443,8 @@ export class ClineProvider maxReadFileLine: maxReadFileLine ?? -1, maxConcurrentFileReads: maxConcurrentFileReads ?? 5, includeDiagnostics: includeDiagnostics ?? false, - maxDiagnosticsCount: maxDiagnosticsCount ?? 5, - diagnosticsFilter: diagnosticsFilter ?? ["error", "warning"], + maxDiagnosticsCount: maxDiagnosticsCount ?? 50, + diagnosticsFilter: diagnosticsFilter ?? [], settingsImportedAt: this.settingsImportedAt, terminalCompressProgressBar: terminalCompressProgressBar ?? true, hasSystemPromptOverride, @@ -1597,8 +1597,8 @@ export class ClineProvider maxReadFileLine: stateValues.maxReadFileLine ?? -1, maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5, includeDiagnostics: stateValues.includeDiagnostics ?? false, - maxDiagnosticsCount: stateValues.maxDiagnosticsCount ?? 5, - diagnosticsFilter: stateValues.diagnosticsFilter ?? ["error", "warning"], + maxDiagnosticsCount: stateValues.maxDiagnosticsCount ?? 50, + diagnosticsFilter: stateValues.diagnosticsFilter ?? [], historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, cloudUserInfo, cloudIsAuthenticated, diff --git a/src/core/webview/__tests__/diagnosticsSettings.integration.test.ts b/src/core/webview/__tests__/diagnosticsSettings.integration.test.ts new file mode 100644 index 0000000000..df335767eb --- /dev/null +++ b/src/core/webview/__tests__/diagnosticsSettings.integration.test.ts @@ -0,0 +1,180 @@ +import * as vscode from "vscode" +import { ClineProvider } from "../ClineProvider" +import { ContextProxy } from "../../config/ContextProxy" + +// Mock vscode +jest.mock("vscode", () => ({ + workspace: { + getConfiguration: jest.fn(() => ({ + get: jest.fn(), + })), + }, + ExtensionContext: jest.fn(), + OutputChannel: jest.fn(), + Uri: { + file: jest.fn((path: string) => ({ fsPath: path })), + }, + ExtensionMode: { + Development: 1, + Production: 2, + Test: 3, + }, +})) + +// Mock other dependencies +jest.mock("../../config/ContextProxy") +jest.mock("../../../services/code-index/manager") +jest.mock("../../../services/mcp/McpServerManager") +jest.mock("../../config/CustomModesManager") +jest.mock("../../../services/marketplace/MarketplaceManager") + +describe("Diagnostics Settings Integration", () => { + let provider: ClineProvider + let mockContext: any + let mockOutputChannel: any + let mockContextProxy: jest.Mocked + + beforeEach(() => { + // Setup mocks + mockContext = { + globalState: { + get: jest.fn(), + update: jest.fn(), + keys: jest.fn(() => []), + }, + secrets: { + get: jest.fn(), + store: jest.fn(), + }, + globalStorageUri: { fsPath: "/test/storage" }, + extensionUri: { fsPath: "/test/extension" }, + extension: { + packageJSON: { + version: "1.0.0", + name: "test-extension", + }, + }, + } + + mockOutputChannel = { + appendLine: jest.fn(), + } + + mockContextProxy = { + getValue: jest.fn(), + setValue: jest.fn(), + getValues: jest.fn(() => ({ + includeDiagnostics: false, + maxDiagnosticsCount: 50, + diagnosticsFilter: [], + })), + setValues: jest.fn(), + getProviderSettings: jest.fn(() => ({})), + setProviderSettings: jest.fn(), + resetAllState: jest.fn(), + extensionUri: { fsPath: "/test/extension" }, + globalStorageUri: { fsPath: "/test/storage" }, + extensionMode: vscode.ExtensionMode.Test, + } as any + + // Create provider instance + provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", mockContextProxy) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("Settings Persistence", () => { + it("should persist includeDiagnostics setting", async () => { + // Simulate setting update + await provider.setValue("includeDiagnostics", true) + + expect(mockContextProxy.setValue).toHaveBeenCalledWith("includeDiagnostics", true) + }) + + it("should persist maxDiagnosticsCount setting", async () => { + // Simulate setting update + await provider.setValue("maxDiagnosticsCount", 100) + + expect(mockContextProxy.setValue).toHaveBeenCalledWith("maxDiagnosticsCount", 100) + }) + + it("should persist diagnosticsFilter setting", async () => { + // Simulate setting update + const filters = ["eslint", "typescript"] + await provider.setValue("diagnosticsFilter", filters) + + expect(mockContextProxy.setValue).toHaveBeenCalledWith("diagnosticsFilter", filters) + }) + + it("should retrieve diagnostics settings from state", () => { + // Setup mock return values + mockContextProxy.getValue.mockImplementation((key: string) => { + const values: Record = { + includeDiagnostics: true, + maxDiagnosticsCount: 75, + diagnosticsFilter: ["error", "warning"], + } + return values[key] + }) + + // Retrieve settings + const includeDiagnostics = provider.getValue("includeDiagnostics") + const maxDiagnosticsCount = provider.getValue("maxDiagnosticsCount") + const diagnosticsFilter = provider.getValue("diagnosticsFilter") + + expect(includeDiagnostics).toBe(true) + expect(maxDiagnosticsCount).toBe(75) + expect(diagnosticsFilter).toEqual(["error", "warning"]) + }) + + it("should update multiple diagnostics settings at once", async () => { + const newSettings = { + includeDiagnostics: true, + maxDiagnosticsCount: 100, + diagnosticsFilter: ["eslint/no-unused-vars", "typescript"], + } + + await provider.setValues(newSettings as any) + + expect(mockContextProxy.setValues).toHaveBeenCalledWith(expect.objectContaining(newSettings)) + }) + }) + + describe("Default Values", () => { + it("should use correct default values when settings are undefined", () => { + mockContextProxy.getValue.mockReturnValue(undefined) + mockContextProxy.getValues.mockReturnValue({}) + + const state = provider.getValues() + + // These defaults should match what's in the code + expect(state.includeDiagnostics ?? false).toBe(false) + expect(state.maxDiagnosticsCount ?? 50).toBe(50) + expect(state.diagnosticsFilter ?? []).toEqual([]) + }) + }) + + describe("Settings Validation", () => { + it("should validate maxDiagnosticsCount range", async () => { + // Test valid range + await provider.setValue("maxDiagnosticsCount", 100) + expect(mockContextProxy.setValue).toHaveBeenCalledWith("maxDiagnosticsCount", 100) + + // Note: Validation should be done in the UI component + // The provider itself doesn't validate ranges + }) + + it("should handle empty diagnosticsFilter", async () => { + await provider.setValue("diagnosticsFilter", []) + expect(mockContextProxy.setValue).toHaveBeenCalledWith("diagnosticsFilter", []) + }) + + it("should handle diagnosticsFilter with multiple values", async () => { + const filters = ["eslint", "typescript", "dart Error", "custom-linter"] + await provider.setValue("diagnosticsFilter", filters) + expect(mockContextProxy.setValue).toHaveBeenCalledWith("diagnosticsFilter", filters) + }) + }) +}) diff --git a/src/integrations/diagnostics/__tests__/diagnosticsToProblemsString.test.ts b/src/integrations/diagnostics/__tests__/diagnosticsToProblemsString.test.ts new file mode 100644 index 0000000000..e546f7d0c4 --- /dev/null +++ b/src/integrations/diagnostics/__tests__/diagnosticsToProblemsString.test.ts @@ -0,0 +1,198 @@ +import * as vscode from "vscode" +import { diagnosticsToProblemsString } from "../index" + +// Mock vscode +jest.mock("vscode", () => ({ + workspace: { + getConfiguration: jest.fn(() => ({ + get: jest.fn((key: string, defaultValue: any) => { + const config: Record = { + includeDiagnostics: false, + maxDiagnosticsCount: 50, + diagnosticsFilter: [], + } + return config[key] ?? defaultValue + }), + })), + fs: { + stat: jest.fn(), + }, + openTextDocument: jest.fn(), + }, + DiagnosticSeverity: { + Error: 0, + Warning: 1, + Information: 2, + Hint: 3, + }, + FileType: { + File: 1, + Directory: 2, + }, + Uri: { + file: (path: string) => ({ fsPath: path }), + }, + Range: jest.fn((startLine: number, startChar: number, endLine: number, endChar: number) => ({ + start: { line: startLine, character: startChar }, + end: { line: endLine, character: endChar }, + })), + Position: jest.fn((line: number, char: number) => ({ line, character: char })), +})) + +describe("diagnosticsToProblemsString", () => { + const mockCwd = "/test/workspace" + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should return empty string when includeDiagnostics is false", async () => { + const diagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [ + [ + vscode.Uri.file("/test/workspace/file.ts"), + [ + { + range: new vscode.Range(0, 0, 0, 10), + message: "Test error", + severity: vscode.DiagnosticSeverity.Error, + } as vscode.Diagnostic, + ], + ], + ] + + const result = await diagnosticsToProblemsString(diagnostics, [vscode.DiagnosticSeverity.Error], mockCwd, { + includeDiagnostics: false, + }) + + expect(result).toBe("") + }) + + it("should include diagnostics when includeDiagnostics is true", async () => { + const mockDocument = { + lineAt: jest.fn(() => ({ text: "const x = 1" })), + } + ;(vscode.workspace.openTextDocument as jest.Mock).mockResolvedValue(mockDocument) + ;(vscode.workspace.fs.stat as jest.Mock).mockResolvedValue({ type: vscode.FileType.File }) + + const diagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [ + [ + vscode.Uri.file("/test/workspace/file.ts"), + [ + { + range: new vscode.Range(0, 0, 0, 10), + message: "Test error", + severity: vscode.DiagnosticSeverity.Error, + source: "typescript", + } as vscode.Diagnostic, + ], + ], + ] + + const result = await diagnosticsToProblemsString(diagnostics, [vscode.DiagnosticSeverity.Error], mockCwd, { + includeDiagnostics: true, + }) + + expect(result).toContain("file.ts") + expect(result).toContain("Test error") + expect(result).toContain("typescript") + }) + + it("should respect maxDiagnosticsCount", async () => { + const mockDocument = { + lineAt: jest.fn(() => ({ text: "const x = 1" })), + } + ;(vscode.workspace.openTextDocument as jest.Mock).mockResolvedValue(mockDocument) + ;(vscode.workspace.fs.stat as jest.Mock).mockResolvedValue({ type: vscode.FileType.File }) + + const diagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [ + [ + vscode.Uri.file("/test/workspace/file.ts"), + Array(10) + .fill(null) + .map( + (_, i) => + ({ + range: new vscode.Range(i, 0, i, 10), + message: `Test error ${i}`, + severity: vscode.DiagnosticSeverity.Error, + }) as vscode.Diagnostic, + ), + ], + ] + + const result = await diagnosticsToProblemsString(diagnostics, [vscode.DiagnosticSeverity.Error], mockCwd, { + includeDiagnostics: true, + maxDiagnosticsCount: 3, + }) + + expect(result).toContain("Test error 0") + expect(result).toContain("Test error 1") + expect(result).toContain("Test error 2") + expect(result).not.toContain("Test error 3") + expect(result).toContain("7 more diagnostics omitted") + }) + + it("should apply diagnosticsFilter correctly", async () => { + const mockDocument = { + lineAt: jest.fn(() => ({ text: "const x = 1" })), + } + ;(vscode.workspace.openTextDocument as jest.Mock).mockResolvedValue(mockDocument) + ;(vscode.workspace.fs.stat as jest.Mock).mockResolvedValue({ type: vscode.FileType.File }) + + const diagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [ + [ + vscode.Uri.file("/test/workspace/file.ts"), + [ + { + range: new vscode.Range(0, 0, 0, 10), + message: "ESLint error", + severity: vscode.DiagnosticSeverity.Error, + source: "eslint", + code: "no-unused-vars", + } as vscode.Diagnostic, + { + range: new vscode.Range(1, 0, 1, 10), + message: "TypeScript error", + severity: vscode.DiagnosticSeverity.Error, + source: "typescript", + code: "2322", + } as vscode.Diagnostic, + ], + ], + ] + + const result = await diagnosticsToProblemsString(diagnostics, [vscode.DiagnosticSeverity.Error], mockCwd, { + includeDiagnostics: true, + diagnosticsFilter: ["typescript 2322"], + }) + + expect(result).toContain("TypeScript error") + expect(result).not.toContain("ESLint error") + }) + + it("should handle file read errors gracefully", async () => { + ;(vscode.workspace.openTextDocument as jest.Mock).mockRejectedValue(new Error("File not found")) + ;(vscode.workspace.fs.stat as jest.Mock).mockResolvedValue({ type: vscode.FileType.File }) + + const diagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [ + [ + vscode.Uri.file("/test/workspace/file.ts"), + [ + { + range: new vscode.Range(0, 0, 0, 10), + message: "Test error", + severity: vscode.DiagnosticSeverity.Error, + } as vscode.Diagnostic, + ], + ], + ] + + const result = await diagnosticsToProblemsString(diagnostics, [vscode.DiagnosticSeverity.Error], mockCwd, { + includeDiagnostics: true, + }) + + expect(result).toContain("file.ts") + expect(result).toContain("(unavailable)") + expect(result).toContain("Test error") + }) +}) diff --git a/src/integrations/diagnostics/index.ts b/src/integrations/diagnostics/index.ts index 0765f630a6..df4d139183 100644 --- a/src/integrations/diagnostics/index.ts +++ b/src/integrations/diagnostics/index.ts @@ -81,20 +81,15 @@ export async function diagnosticsToProblemsString( }, ): Promise { // Use provided options or fall back to VSCode configuration - const includeDiagnostics = - options?.includeDiagnostics ?? - vscode.workspace.getConfiguration("roo-cline").get("includeDiagnostics", false) + const config = vscode.workspace.getConfiguration("roo-cline") + const includeDiagnostics = options?.includeDiagnostics ?? config.get("includeDiagnostics", false) if (!includeDiagnostics) { return "" } - const maxDiagnosticsCount = - options?.maxDiagnosticsCount ?? - vscode.workspace.getConfiguration("roo-cline").get("maxDiagnosticsCount", 5) - const diagnosticsFilter = - options?.diagnosticsFilter ?? - vscode.workspace.getConfiguration("roo-cline").get("diagnosticsFilter", ["error", "warning"]) + const maxDiagnosticsCount = options?.maxDiagnosticsCount ?? config.get("maxDiagnosticsCount", 50) + const diagnosticsFilter = options?.diagnosticsFilter ?? config.get("diagnosticsFilter", []) const documents = new Map() const fileStats = new Map() @@ -112,10 +107,10 @@ export async function diagnosticsToProblemsString( const code = typeof d.code === "object" ? d.code.value : d.code const filterKey = source ? `${source} ${code || ""}`.trim() : `${code || ""}`.trim() - // Check if this diagnostic should be filtered out - return !diagnosticsFilter.some((filter) => { - // Support partial matching - return filterKey.includes(filter) || d.message.includes(filter) + // Check if this diagnostic matches any filter (exact match) + return diagnosticsFilter.some((filter) => { + // Exact matching for filter key + return filterKey === filter || (filter && filterKey.startsWith(filter + " ")) }) }) .sort((a, b) => a.range.start.line - b.range.start.line) diff --git a/src/package.json b/src/package.json index 77ce1a1240..8212f3da17 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "RooVeterinaryInc", - "version": "3.20.3", + "version": "3.20.4", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", @@ -347,7 +347,7 @@ }, "roo-cline.includeDiagnostics": { "type": "boolean", - "default": true, + "default": false, "description": "%settings.includeDiagnostics.description%" }, "roo-cline.maxDiagnosticsCount": { diff --git a/webview-ui/src/components/settings/DiagnosticsSettings.tsx b/webview-ui/src/components/settings/DiagnosticsSettings.tsx index bfe3f2173f..51ae94a65e 100644 --- a/webview-ui/src/components/settings/DiagnosticsSettings.tsx +++ b/webview-ui/src/components/settings/DiagnosticsSettings.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes } from "react" +import { HTMLAttributes, ChangeEvent } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { AlertCircle } from "lucide-react" @@ -48,7 +48,9 @@ export const DiagnosticsSettings = ({
setCachedStateField("includeDiagnostics", e.target.checked)} + onChange={(e: ChangeEvent) => + setCachedStateField("includeDiagnostics", e.target.checked) + } data-testid="include-diagnostics-checkbox">