From 6ab504a36f3bf4f0088db549b569ec7ba0d73304 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Sat, 14 Jun 2025 18:41:16 -0600 Subject: [PATCH] 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 = {