feat: add toggle for single-file read mode to fix model confusion

- Add useSingleFileReadMode setting to global settings schema
- Update shouldUseSingleFileRead function to check user preference
- Add UI toggle in Context settings section
- Add comprehensive test coverage
- Fixes issue #8848 where models like Qwen3-Coder get confused by multi-file args format
This commit is contained in:
Roo Code 2025-10-26 18:11:18 +00:00
parent f5d7ba1959
commit 5e8a771f4f
15 changed files with 157 additions and 3 deletions

View file

@ -0,0 +1,96 @@
import { describe, it, expect } from "vitest"
import { shouldUseSingleFileRead } from "../single-file-read-models.js"
describe("shouldUseSingleFileRead", () => {
describe("with user setting", () => {
it("should return true when useSingleFileReadMode is true, regardless of model", () => {
// Test various models when user setting is enabled
expect(shouldUseSingleFileRead("claude-3-5-sonnet", true)).toBe(true)
expect(shouldUseSingleFileRead("gpt-4", true)).toBe(true)
expect(shouldUseSingleFileRead("qwen-coder", true)).toBe(true)
expect(shouldUseSingleFileRead("any-random-model", true)).toBe(true)
})
it("should return false when useSingleFileReadMode is false and model is not in the list", () => {
// Test models that are not in the single-file list when user setting is disabled
expect(shouldUseSingleFileRead("claude-3-5-sonnet", false)).toBe(false)
expect(shouldUseSingleFileRead("gpt-4", false)).toBe(false)
expect(shouldUseSingleFileRead("qwen-coder", false)).toBe(false)
})
it("should respect false setting even for models that normally use single-file mode", () => {
// When user explicitly sets to false, it should override model defaults
expect(shouldUseSingleFileRead("grok-code-fast-1", false)).toBe(false)
expect(shouldUseSingleFileRead("code-supernova", false)).toBe(false)
expect(shouldUseSingleFileRead("some-model-with-grok-code-fast-1-in-name", false)).toBe(false)
})
})
describe("without user setting (undefined)", () => {
it("should return true for models that include the special strings", () => {
// Exact matches
expect(shouldUseSingleFileRead("grok-code-fast-1", undefined)).toBe(true)
expect(shouldUseSingleFileRead("code-supernova", undefined)).toBe(true)
// Models that contain the special strings
expect(shouldUseSingleFileRead("x/grok-code-fast-1", undefined)).toBe(true)
expect(shouldUseSingleFileRead("provider/code-supernova-v2", undefined)).toBe(true)
expect(shouldUseSingleFileRead("grok-code-fast-1-turbo", undefined)).toBe(true)
})
it("should return false for models not in the single-file list", () => {
expect(shouldUseSingleFileRead("claude-3-5-sonnet", undefined)).toBe(false)
expect(shouldUseSingleFileRead("gpt-4", undefined)).toBe(false)
expect(shouldUseSingleFileRead("gemini-pro", undefined)).toBe(false)
expect(shouldUseSingleFileRead("any-other-model", undefined)).toBe(false)
expect(shouldUseSingleFileRead("", undefined)).toBe(false)
})
it("should return false when no parameters are provided", () => {
expect(shouldUseSingleFileRead(undefined, undefined)).toBe(false)
})
})
describe("edge cases", () => {
it("should handle empty model string", () => {
expect(shouldUseSingleFileRead("", true)).toBe(true) // User setting takes precedence
expect(shouldUseSingleFileRead("", false)).toBe(false)
expect(shouldUseSingleFileRead("", undefined)).toBe(false)
})
it("should handle undefined model", () => {
expect(shouldUseSingleFileRead(undefined, true)).toBe(true) // User setting takes precedence
expect(shouldUseSingleFileRead(undefined, false)).toBe(false)
expect(shouldUseSingleFileRead(undefined, undefined)).toBe(false)
})
it("should handle partial model name matches correctly", () => {
// The function uses includes(), so partial matches matter
expect(shouldUseSingleFileRead("grok-code", undefined)).toBe(false) // Doesn't include full "grok-code-fast-1"
expect(shouldUseSingleFileRead("code-fast-1", undefined)).toBe(false) // Doesn't include full "grok-code-fast-1"
expect(shouldUseSingleFileRead("supernova", undefined)).toBe(false) // Doesn't include full "code-supernova"
expect(shouldUseSingleFileRead("grok", undefined)).toBe(false) // Too short
expect(shouldUseSingleFileRead("code", undefined)).toBe(false) // Too short
})
it("should be case-sensitive", () => {
// The function uses includes() which is case-sensitive
expect(shouldUseSingleFileRead("GROK-CODE-FAST-1", undefined)).toBe(false)
expect(shouldUseSingleFileRead("Code-Supernova", undefined)).toBe(false)
expect(shouldUseSingleFileRead("Grok-Code-Fast-1", undefined)).toBe(false)
expect(shouldUseSingleFileRead("CODE-SUPERNOVA", undefined)).toBe(false)
})
})
describe("user preference priority", () => {
it("should always prioritize explicit user preference over model defaults", () => {
// User explicitly wants single-file mode for a model that doesn't require it
expect(shouldUseSingleFileRead("claude-3-5-sonnet", true)).toBe(true)
// User explicitly doesn't want single-file mode for models that typically require it
expect(shouldUseSingleFileRead("grok-code-fast-1", false)).toBe(false)
expect(shouldUseSingleFileRead("code-supernova", false)).toBe(false)
expect(shouldUseSingleFileRead("provider/grok-code-fast-1-latest", false)).toBe(false)
})
})
})

View file

@ -130,6 +130,7 @@ export const globalSettingsSchema = z.object({
maxReadFileLine: z.number().optional(),
maxImageFileSize: z.number().optional(),
maxTotalImageSize: z.number().optional(),
useSingleFileReadMode: z.boolean().optional(),
terminalOutputLineLimit: z.number().optional(),
terminalOutputCharacterLimit: z.number().optional(),

View file

@ -7,8 +7,20 @@
/**
* Check if a model should use single file read format
* @param modelId The model ID to check
* @param useSingleFileReadMode Optional user preference to force single file mode
* @returns true if the model should use single file reads
*/
export function shouldUseSingleFileRead(modelId: string): boolean {
export function shouldUseSingleFileRead(modelId: string | undefined, useSingleFileReadMode?: boolean): boolean {
// If user has explicitly set the preference, use it (both true and false)
if (useSingleFileReadMode !== undefined) {
return useSingleFileReadMode
}
// If no modelId provided, default to false
if (!modelId) {
return false
}
// Otherwise, check if the model is known to have issues with multi-file format
return modelId.includes("grok-code-fast-1") || modelId.includes("code-supernova")
}

View file

@ -470,7 +470,8 @@ export async function presentAssistantMessage(cline: Task) {
case "read_file":
// Check if this model should use the simplified single-file read tool
const modelId = cline.api.getModel().id
if (shouldUseSingleFileRead(modelId)) {
const useSingleFileReadMode = (await cline.providerRef.deref()?.getState())?.useSingleFileReadMode
if (shouldUseSingleFileRead(modelId, useSingleFileReadMode)) {
await simpleReadFileTool(
cline,
block,

View file

@ -35,7 +35,8 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
read_file: (args) => {
// Check if the current model should use the simplified read_file tool
const modelId = args.settings?.modelId
if (modelId && shouldUseSingleFileRead(modelId)) {
const useSingleFileReadMode = args.settings?.useSingleFileReadMode
if (modelId && shouldUseSingleFileRead(modelId, useSingleFileReadMode)) {
return getSimpleReadFileDescription(args)
}
return getReadFileDescription(args)

View file

@ -1794,6 +1794,7 @@ export class ClineProvider
maxReadFileLine,
maxImageFileSize,
maxTotalImageSize,
useSingleFileReadMode,
terminalCompressProgressBar,
historyPreviewCollapsed,
reasoningBlockCollapsed,
@ -1924,6 +1925,7 @@ export class ClineProvider
maxReadFileLine: maxReadFileLine ?? -1,
maxImageFileSize: maxImageFileSize ?? 5,
maxTotalImageSize: maxTotalImageSize ?? 20,
useSingleFileReadMode: useSingleFileReadMode ?? false,
maxConcurrentFileReads: maxConcurrentFileReads ?? 5,
settingsImportedAt: this.settingsImportedAt,
terminalCompressProgressBar: terminalCompressProgressBar ?? true,
@ -2143,6 +2145,7 @@ export class ClineProvider
maxReadFileLine: stateValues.maxReadFileLine ?? -1,
maxImageFileSize: stateValues.maxImageFileSize ?? 5,
maxTotalImageSize: stateValues.maxTotalImageSize ?? 20,
useSingleFileReadMode: stateValues.useSingleFileReadMode ?? false,
maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,

View file

@ -562,6 +562,7 @@ describe("ClineProvider", () => {
taskSyncEnabled: false,
featureRoomoteControlEnabled: false,
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
useSingleFileReadMode: false,
}
const message: ExtensionMessage = {

View file

@ -1639,6 +1639,10 @@ export const webviewMessageHandler = async (
await updateGlobalState("maxReadFileLine", message.value)
await provider.postStateToWebview()
break
case "useSingleFileReadMode":
await updateGlobalState("useSingleFileReadMode", message.bool ?? false)
await provider.postStateToWebview()
break
case "maxImageFileSize":
await updateGlobalState("maxImageFileSize", message.value)
await provider.postStateToWebview()

View file

@ -316,6 +316,7 @@ export type ExtensionState = Pick<
maxReadFileLine: number // Maximum number of lines to read from a file before truncating
maxImageFileSize: number // Maximum size of image files to process in MB
maxTotalImageSize: number // Maximum total size for all images in a single read operation in MB
useSingleFileReadMode: boolean // Force use of single-file read mode for models that struggle with multi-file args
experiments: Experiments // Map of experiment IDs to their enabled state

View file

@ -174,6 +174,7 @@ export interface WebviewMessage {
| "maxReadFileLine"
| "maxImageFileSize"
| "maxTotalImageSize"
| "useSingleFileReadMode"
| "maxConcurrentFileReads"
| "includeDiagnosticMessages"
| "maxDiagnosticMessages"

View file

@ -22,6 +22,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
maxReadFileLine?: number
maxImageFileSize?: number
maxTotalImageSize?: number
useSingleFileReadMode?: boolean
maxConcurrentFileReads?: number
profileThresholds?: Record<string, number>
includeDiagnosticMessages?: boolean
@ -36,6 +37,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
| "maxReadFileLine"
| "maxImageFileSize"
| "maxTotalImageSize"
| "useSingleFileReadMode"
| "maxConcurrentFileReads"
| "profileThresholds"
| "includeDiagnosticMessages"
@ -55,6 +57,7 @@ export const ContextManagementSettings = ({
maxReadFileLine,
maxImageFileSize,
maxTotalImageSize,
useSingleFileReadMode,
maxConcurrentFileReads,
profileThresholds = {},
includeDiagnosticMessages,
@ -435,6 +438,25 @@ export const ContextManagementSettings = ({
: t("settings:contextManagement.condensingThreshold.profileDescription")}
</div>
</div>
<div className="mb-6 p-4 bg-vscode-editor-inactiveSelectionBackground rounded-lg border border-vscode-editorWidget-border">
<div className="flex flex-col gap-2">
<span className="font-medium">
{t("settings:contextManagement.useSingleFileReadMode.label")}
</span>
<VSCodeCheckbox
checked={useSingleFileReadMode ?? false}
onChange={(e: any) =>
setCachedStateField("useSingleFileReadMode", e.target.checked)
}
data-testid="use-single-file-read-mode-checkbox">
{t("settings:contextManagement.useSingleFileReadMode.checkboxLabel")}
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-2">
{t("settings:contextManagement.useSingleFileReadMode.description")}
</div>
</div>
</div>
</div>
)}
</Section>

View file

@ -181,6 +181,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
maxReadFileLine,
maxImageFileSize,
maxTotalImageSize,
useSingleFileReadMode,
terminalCompressProgressBar,
maxConcurrentFileReads,
condensingApiConfigId,
@ -371,6 +372,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
vscode.postMessage({ type: "maxReadFileLine", value: maxReadFileLine ?? -1 })
vscode.postMessage({ type: "maxImageFileSize", value: maxImageFileSize ?? 5 })
vscode.postMessage({ type: "maxTotalImageSize", value: maxTotalImageSize ?? 20 })
vscode.postMessage({ type: "useSingleFileReadMode", bool: useSingleFileReadMode ?? false })
vscode.postMessage({ type: "maxConcurrentFileReads", value: cachedState.maxConcurrentFileReads ?? 5 })
vscode.postMessage({ type: "includeDiagnosticMessages", bool: includeDiagnosticMessages })
vscode.postMessage({ type: "maxDiagnosticMessages", value: maxDiagnosticMessages ?? 50 })

View file

@ -140,6 +140,8 @@ export interface ExtensionStateContextType extends ExtensionState {
setMaxImageFileSize: (value: number) => void
maxTotalImageSize: number
setMaxTotalImageSize: (value: number) => void
useSingleFileReadMode: boolean
setUseSingleFileReadMode: (value: boolean) => void
machineId?: string
pinnedApiConfigs?: Record<string, boolean>
setPinnedApiConfigs: (value: Record<string, boolean>) => void
@ -238,6 +240,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
maxReadFileLine: -1, // Default max read file line limit
maxImageFileSize: 5, // Default max image file size in MB
maxTotalImageSize: 20, // Default max total image size in MB
useSingleFileReadMode: false, // Default to multi-file mode
pinnedApiConfigs: {}, // Empty object for pinned API configs
terminalZshOhMy: false, // Default Oh My Zsh integration setting
maxConcurrentFileReads: 5, // Default concurrent file reads
@ -531,6 +534,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setMaxReadFileLine: (value) => setState((prevState) => ({ ...prevState, maxReadFileLine: value })),
setMaxImageFileSize: (value) => setState((prevState) => ({ ...prevState, maxImageFileSize: value })),
setMaxTotalImageSize: (value) => setState((prevState) => ({ ...prevState, maxTotalImageSize: value })),
setUseSingleFileReadMode: (value) => setState((prevState) => ({ ...prevState, useSingleFileReadMode: value })),
setPinnedApiConfigs: (value) => setState((prevState) => ({ ...prevState, pinnedApiConfigs: value })),
setTerminalCompressProgressBar: (value) =>
setState((prevState) => ({ ...prevState, terminalCompressProgressBar: value })),

View file

@ -215,6 +215,7 @@ describe("mergeExtensionState", () => {
taskSyncEnabled: false,
featureRoomoteControlEnabled: false,
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, // Add the checkpoint timeout property
useSingleFileReadMode: false,
}
const prevState: ExtensionState = {

View file

@ -580,6 +580,10 @@
"lines": "lines",
"always_full_read": "Always read entire file"
},
"useSingleFileReadMode": {
"label": "Force single-file read mode",
"description": "When enabled, forces the read_file tool to use single-file mode syntax. This helps with models like Qwen3-Coder that get confused by the multi-file XML args format and incorrectly use it with other tools. Only enable if you're experiencing 'required parameter not provided' errors."
},
"maxImageFileSize": {
"label": "Max image file size",
"mb": "MB",