mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
f5d7ba1959
commit
5e8a771f4f
15 changed files with 157 additions and 3 deletions
96
packages/types/src/__tests__/single-file-read-models.test.ts
Normal file
96
packages/types/src/__tests__/single-file-read-models.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -562,6 +562,7 @@ describe("ClineProvider", () => {
|
|||
taskSyncEnabled: false,
|
||||
featureRoomoteControlEnabled: false,
|
||||
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
|
||||
useSingleFileReadMode: false,
|
||||
}
|
||||
|
||||
const message: ExtensionMessage = {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ export interface WebviewMessage {
|
|||
| "maxReadFileLine"
|
||||
| "maxImageFileSize"
|
||||
| "maxTotalImageSize"
|
||||
| "useSingleFileReadMode"
|
||||
| "maxConcurrentFileReads"
|
||||
| "includeDiagnosticMessages"
|
||||
| "maxDiagnosticMessages"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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 })),
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue