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.
This commit is contained in:
hannesrudolph 2025-06-14 18:41:16 -06:00
parent 54edab571a
commit 6ab504a36f
13 changed files with 236 additions and 1 deletions

View file

@ -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(),

View file

@ -246,6 +246,14 @@ async function getFileOrFolderContent(
}
async function getWorkspaceProblems(cwd: string): Promise<string> {
// Check if diagnostics are enabled
const config = vscode.workspace.getConfiguration("roo-cline")
const includeDiagnostics = config.get<boolean>("includeDiagnostics", true)
if (!includeDiagnostics) {
return "Diagnostics are disabled in settings."
}
const diagnostics = vscode.languages.getDiagnostics()
const result = await diagnosticsToProblemsString(
diagnostics,

View file

@ -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,

View file

@ -432,6 +432,9 @@ describe("ClineProvider", () => {
autoCondenseContextPercent: 100,
cloudIsAuthenticated: false,
sharingEnabled: false,
includeDiagnostics: false,
maxDiagnosticsCount: 5,
diagnosticsFilter: ["error", "warning"],
}
const message: ExtensionMessage = {

View file

@ -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

View file

@ -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<string> {
// Use provided options or fall back to VSCode configuration
const includeDiagnostics =
options?.includeDiagnostics ??
vscode.workspace.getConfiguration("roo-cline").get<boolean>("includeDiagnostics", false)
if (!includeDiagnostics) {
return ""
}
const maxDiagnosticsCount =
options?.maxDiagnosticsCount ??
vscode.workspace.getConfiguration("roo-cline").get<number>("maxDiagnosticsCount", 5)
const diagnosticsFilter =
options?.diagnosticsFilter ??
vscode.workspace.getConfiguration("roo-cline").get<string[]>("diagnosticsFilter", ["error", "warning"])
const documents = new Map<vscode.Uri, vscode.TextDocument>()
const fileStats = new Map<vscode.Uri, vscode.FileStat>()
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++
}
}
}

View file

@ -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%"
}
}
}

View file

@ -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."
}

View file

@ -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

View file

@ -144,6 +144,9 @@ export interface WebviewMessage {
| "language"
| "maxReadFileLine"
| "maxConcurrentFileReads"
| "includeDiagnostics"
| "maxDiagnosticsCount"
| "diagnosticsFilter"
| "searchFiles"
| "toggleApiConfigPin"
| "setHistoryPreviewCollapsed"

View file

@ -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<HTMLDivElement> & {
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 (
<div className={cn("flex flex-col gap-2", className)} {...props}>
<SectionHeader description={t("settings:diagnostics.description")}>
<div className="flex items-center gap-2">
<AlertCircle className="w-4" />
<div>{t("settings:sections.diagnostics")}</div>
</div>
</SectionHeader>
<Section>
<div>
<VSCodeCheckbox
checked={includeDiagnostics}
onChange={(e: any) => setCachedStateField("includeDiagnostics", e.target.checked)}
data-testid="include-diagnostics-checkbox">
<label className="block font-medium mb-1">
{t("settings:diagnostics.includeDiagnostics.label")}
</label>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
{t("settings:diagnostics.includeDiagnostics.description")}
</div>
</div>
{includeDiagnostics && (
<div className="flex flex-col gap-4 pl-3 border-l-2 border-vscode-button-background">
<div>
<span className="block font-medium mb-1">
{t("settings:diagnostics.maxDiagnosticsCount.label")}
</span>
<div className="flex items-center gap-2">
<Slider
min={0}
max={200}
step={1}
value={[maxDiagnosticsCount ?? 50]}
onValueChange={([value]) => setCachedStateField("maxDiagnosticsCount", value)}
data-testid="max-diagnostics-count-slider"
/>
<span className="w-10">{maxDiagnosticsCount ?? 50}</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:diagnostics.maxDiagnosticsCount.description")}
</div>
</div>
<div>
<span className="block font-medium mb-1">
{t("settings:diagnostics.diagnosticsFilter.label")}
</span>
<Input
type="text"
className="w-full bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border px-2 py-1 rounded"
value={diagnosticsFilter?.join(", ") || ""}
onChange={(e) => handleFilterChange(e.target.value)}
placeholder="e.g., dart Error, eslint/no-unused-vars"
data-testid="diagnostics-filter-input"
/>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:diagnostics.diagnosticsFilter.description")}
</div>
</div>
</div>
)}
</Section>
</div>
)
}

View file

@ -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<ExtensionStateContextType | undefined>(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 <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

View file

@ -209,6 +209,9 @@ describe("mergeExtensionState", () => {
autoCondenseContextPercent: 100,
cloudIsAuthenticated: false,
sharingEnabled: false,
includeDiagnostics: false,
maxDiagnosticsCount: 5,
diagnosticsFilter: ["error", "warning"],
}
const prevState: ExtensionState = {