Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions packages/types/src/__tests__/single-file-read-models.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
14 changes: 13 additions & 1 deletion packages/types/src/single-file-read-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
3 changes: 2 additions & 1 deletion src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/core/prompts/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1794,6 +1794,7 @@ export class ClineProvider
maxReadFileLine,
maxImageFileSize,
maxTotalImageSize,
useSingleFileReadMode,
terminalCompressProgressBar,
historyPreviewCollapsed,
reasoningBlockCollapsed,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ describe("ClineProvider", () => {
taskSyncEnabled: false,
featureRoomoteControlEnabled: false,
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
useSingleFileReadMode: false,
}

const message: ExtensionMessage = {
Expand Down
4 changes: 4 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export interface WebviewMessage {
| "maxReadFileLine"
| "maxImageFileSize"
| "maxTotalImageSize"
| "useSingleFileReadMode"
| "maxConcurrentFileReads"
| "includeDiagnosticMessages"
| "maxDiagnosticMessages"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
maxReadFileLine?: number
maxImageFileSize?: number
maxTotalImageSize?: number
useSingleFileReadMode?: boolean
maxConcurrentFileReads?: number
profileThresholds?: Record<string, number>
includeDiagnosticMessages?: boolean
Expand All @@ -36,6 +37,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
| "maxReadFileLine"
| "maxImageFileSize"
| "maxTotalImageSize"
| "useSingleFileReadMode"
| "maxConcurrentFileReads"
| "profileThresholds"
| "includeDiagnosticMessages"
Expand All @@ -55,6 +57,7 @@ export const ContextManagementSettings = ({
maxReadFileLine,
maxImageFileSize,
maxTotalImageSize,
useSingleFileReadMode,
maxConcurrentFileReads,
profileThresholds = {},
includeDiagnosticMessages,
Expand Down Expand Up @@ -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>
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
maxReadFileLine,
maxImageFileSize,
maxTotalImageSize,
useSingleFileReadMode,
terminalCompressProgressBar,
maxConcurrentFileReads,
condensingApiConfigId,
Expand Down Expand Up @@ -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 })
Expand Down
4 changes: 4 additions & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading