Skip to content

Commit 5e8a771

Browse files
committed
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
1 parent f5d7ba1 commit 5e8a771

File tree

15 files changed

+157
-3
lines changed

15 files changed

+157
-3
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { describe, it, expect } from "vitest"
2+
import { shouldUseSingleFileRead } from "../single-file-read-models.js"
3+
4+
describe("shouldUseSingleFileRead", () => {
5+
describe("with user setting", () => {
6+
it("should return true when useSingleFileReadMode is true, regardless of model", () => {
7+
// Test various models when user setting is enabled
8+
expect(shouldUseSingleFileRead("claude-3-5-sonnet", true)).toBe(true)
9+
expect(shouldUseSingleFileRead("gpt-4", true)).toBe(true)
10+
expect(shouldUseSingleFileRead("qwen-coder", true)).toBe(true)
11+
expect(shouldUseSingleFileRead("any-random-model", true)).toBe(true)
12+
})
13+
14+
it("should return false when useSingleFileReadMode is false and model is not in the list", () => {
15+
// Test models that are not in the single-file list when user setting is disabled
16+
expect(shouldUseSingleFileRead("claude-3-5-sonnet", false)).toBe(false)
17+
expect(shouldUseSingleFileRead("gpt-4", false)).toBe(false)
18+
expect(shouldUseSingleFileRead("qwen-coder", false)).toBe(false)
19+
})
20+
21+
it("should respect false setting even for models that normally use single-file mode", () => {
22+
// When user explicitly sets to false, it should override model defaults
23+
expect(shouldUseSingleFileRead("grok-code-fast-1", false)).toBe(false)
24+
expect(shouldUseSingleFileRead("code-supernova", false)).toBe(false)
25+
expect(shouldUseSingleFileRead("some-model-with-grok-code-fast-1-in-name", false)).toBe(false)
26+
})
27+
})
28+
29+
describe("without user setting (undefined)", () => {
30+
it("should return true for models that include the special strings", () => {
31+
// Exact matches
32+
expect(shouldUseSingleFileRead("grok-code-fast-1", undefined)).toBe(true)
33+
expect(shouldUseSingleFileRead("code-supernova", undefined)).toBe(true)
34+
35+
// Models that contain the special strings
36+
expect(shouldUseSingleFileRead("x/grok-code-fast-1", undefined)).toBe(true)
37+
expect(shouldUseSingleFileRead("provider/code-supernova-v2", undefined)).toBe(true)
38+
expect(shouldUseSingleFileRead("grok-code-fast-1-turbo", undefined)).toBe(true)
39+
})
40+
41+
it("should return false for models not in the single-file list", () => {
42+
expect(shouldUseSingleFileRead("claude-3-5-sonnet", undefined)).toBe(false)
43+
expect(shouldUseSingleFileRead("gpt-4", undefined)).toBe(false)
44+
expect(shouldUseSingleFileRead("gemini-pro", undefined)).toBe(false)
45+
expect(shouldUseSingleFileRead("any-other-model", undefined)).toBe(false)
46+
expect(shouldUseSingleFileRead("", undefined)).toBe(false)
47+
})
48+
49+
it("should return false when no parameters are provided", () => {
50+
expect(shouldUseSingleFileRead(undefined, undefined)).toBe(false)
51+
})
52+
})
53+
54+
describe("edge cases", () => {
55+
it("should handle empty model string", () => {
56+
expect(shouldUseSingleFileRead("", true)).toBe(true) // User setting takes precedence
57+
expect(shouldUseSingleFileRead("", false)).toBe(false)
58+
expect(shouldUseSingleFileRead("", undefined)).toBe(false)
59+
})
60+
61+
it("should handle undefined model", () => {
62+
expect(shouldUseSingleFileRead(undefined, true)).toBe(true) // User setting takes precedence
63+
expect(shouldUseSingleFileRead(undefined, false)).toBe(false)
64+
expect(shouldUseSingleFileRead(undefined, undefined)).toBe(false)
65+
})
66+
67+
it("should handle partial model name matches correctly", () => {
68+
// The function uses includes(), so partial matches matter
69+
expect(shouldUseSingleFileRead("grok-code", undefined)).toBe(false) // Doesn't include full "grok-code-fast-1"
70+
expect(shouldUseSingleFileRead("code-fast-1", undefined)).toBe(false) // Doesn't include full "grok-code-fast-1"
71+
expect(shouldUseSingleFileRead("supernova", undefined)).toBe(false) // Doesn't include full "code-supernova"
72+
expect(shouldUseSingleFileRead("grok", undefined)).toBe(false) // Too short
73+
expect(shouldUseSingleFileRead("code", undefined)).toBe(false) // Too short
74+
})
75+
76+
it("should be case-sensitive", () => {
77+
// The function uses includes() which is case-sensitive
78+
expect(shouldUseSingleFileRead("GROK-CODE-FAST-1", undefined)).toBe(false)
79+
expect(shouldUseSingleFileRead("Code-Supernova", undefined)).toBe(false)
80+
expect(shouldUseSingleFileRead("Grok-Code-Fast-1", undefined)).toBe(false)
81+
expect(shouldUseSingleFileRead("CODE-SUPERNOVA", undefined)).toBe(false)
82+
})
83+
})
84+
85+
describe("user preference priority", () => {
86+
it("should always prioritize explicit user preference over model defaults", () => {
87+
// User explicitly wants single-file mode for a model that doesn't require it
88+
expect(shouldUseSingleFileRead("claude-3-5-sonnet", true)).toBe(true)
89+
90+
// User explicitly doesn't want single-file mode for models that typically require it
91+
expect(shouldUseSingleFileRead("grok-code-fast-1", false)).toBe(false)
92+
expect(shouldUseSingleFileRead("code-supernova", false)).toBe(false)
93+
expect(shouldUseSingleFileRead("provider/grok-code-fast-1-latest", false)).toBe(false)
94+
})
95+
})
96+
})

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export const globalSettingsSchema = z.object({
130130
maxReadFileLine: z.number().optional(),
131131
maxImageFileSize: z.number().optional(),
132132
maxTotalImageSize: z.number().optional(),
133+
useSingleFileReadMode: z.boolean().optional(),
133134

134135
terminalOutputLineLimit: z.number().optional(),
135136
terminalOutputCharacterLimit: z.number().optional(),

packages/types/src/single-file-read-models.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,20 @@
77
/**
88
* Check if a model should use single file read format
99
* @param modelId The model ID to check
10+
* @param useSingleFileReadMode Optional user preference to force single file mode
1011
* @returns true if the model should use single file reads
1112
*/
12-
export function shouldUseSingleFileRead(modelId: string): boolean {
13+
export function shouldUseSingleFileRead(modelId: string | undefined, useSingleFileReadMode?: boolean): boolean {
14+
// If user has explicitly set the preference, use it (both true and false)
15+
if (useSingleFileReadMode !== undefined) {
16+
return useSingleFileReadMode
17+
}
18+
19+
// If no modelId provided, default to false
20+
if (!modelId) {
21+
return false
22+
}
23+
24+
// Otherwise, check if the model is known to have issues with multi-file format
1325
return modelId.includes("grok-code-fast-1") || modelId.includes("code-supernova")
1426
}

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,8 @@ export async function presentAssistantMessage(cline: Task) {
470470
case "read_file":
471471
// Check if this model should use the simplified single-file read tool
472472
const modelId = cline.api.getModel().id
473-
if (shouldUseSingleFileRead(modelId)) {
473+
const useSingleFileReadMode = (await cline.providerRef.deref()?.getState())?.useSingleFileReadMode
474+
if (shouldUseSingleFileRead(modelId, useSingleFileReadMode)) {
474475
await simpleReadFileTool(
475476
cline,
476477
block,

src/core/prompts/tools/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
3535
read_file: (args) => {
3636
// Check if the current model should use the simplified read_file tool
3737
const modelId = args.settings?.modelId
38-
if (modelId && shouldUseSingleFileRead(modelId)) {
38+
const useSingleFileReadMode = args.settings?.useSingleFileReadMode
39+
if (modelId && shouldUseSingleFileRead(modelId, useSingleFileReadMode)) {
3940
return getSimpleReadFileDescription(args)
4041
}
4142
return getReadFileDescription(args)

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1794,6 +1794,7 @@ export class ClineProvider
17941794
maxReadFileLine,
17951795
maxImageFileSize,
17961796
maxTotalImageSize,
1797+
useSingleFileReadMode,
17971798
terminalCompressProgressBar,
17981799
historyPreviewCollapsed,
17991800
reasoningBlockCollapsed,
@@ -1924,6 +1925,7 @@ export class ClineProvider
19241925
maxReadFileLine: maxReadFileLine ?? -1,
19251926
maxImageFileSize: maxImageFileSize ?? 5,
19261927
maxTotalImageSize: maxTotalImageSize ?? 20,
1928+
useSingleFileReadMode: useSingleFileReadMode ?? false,
19271929
maxConcurrentFileReads: maxConcurrentFileReads ?? 5,
19281930
settingsImportedAt: this.settingsImportedAt,
19291931
terminalCompressProgressBar: terminalCompressProgressBar ?? true,
@@ -2143,6 +2145,7 @@ export class ClineProvider
21432145
maxReadFileLine: stateValues.maxReadFileLine ?? -1,
21442146
maxImageFileSize: stateValues.maxImageFileSize ?? 5,
21452147
maxTotalImageSize: stateValues.maxTotalImageSize ?? 20,
2148+
useSingleFileReadMode: stateValues.useSingleFileReadMode ?? false,
21462149
maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,
21472150
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
21482151
reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,

src/core/webview/__tests__/ClineProvider.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ describe("ClineProvider", () => {
562562
taskSyncEnabled: false,
563563
featureRoomoteControlEnabled: false,
564564
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
565+
useSingleFileReadMode: false,
565566
}
566567

567568
const message: ExtensionMessage = {

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1639,6 +1639,10 @@ export const webviewMessageHandler = async (
16391639
await updateGlobalState("maxReadFileLine", message.value)
16401640
await provider.postStateToWebview()
16411641
break
1642+
case "useSingleFileReadMode":
1643+
await updateGlobalState("useSingleFileReadMode", message.bool ?? false)
1644+
await provider.postStateToWebview()
1645+
break
16421646
case "maxImageFileSize":
16431647
await updateGlobalState("maxImageFileSize", message.value)
16441648
await provider.postStateToWebview()

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ export type ExtensionState = Pick<
316316
maxReadFileLine: number // Maximum number of lines to read from a file before truncating
317317
maxImageFileSize: number // Maximum size of image files to process in MB
318318
maxTotalImageSize: number // Maximum total size for all images in a single read operation in MB
319+
useSingleFileReadMode: boolean // Force use of single-file read mode for models that struggle with multi-file args
319320

320321
experiments: Experiments // Map of experiment IDs to their enabled state
321322

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ export interface WebviewMessage {
174174
| "maxReadFileLine"
175175
| "maxImageFileSize"
176176
| "maxTotalImageSize"
177+
| "useSingleFileReadMode"
177178
| "maxConcurrentFileReads"
178179
| "includeDiagnosticMessages"
179180
| "maxDiagnosticMessages"

0 commit comments

Comments
 (0)