diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index bf3364d38d7..cf643bfd3a3 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -189,6 +189,9 @@ const baseProviderSettingsSchema = z.object({ // Model verbosity. verbosity: verbosityLevelsSchema.optional(), + + // File reading limits. + maxReadFileLine: z.number().int().min(1).optional(), }) // Several of the providers share common model config properties. diff --git a/src/core/mentions/__tests__/processUserContentMentions.spec.ts b/src/core/mentions/__tests__/processUserContentMentions.spec.ts index 7732cf279b4..77cb0e46021 100644 --- a/src/core/mentions/__tests__/processUserContentMentions.spec.ts +++ b/src/core/mentions/__tests__/processUserContentMentions.spec.ts @@ -223,6 +223,7 @@ describe("processUserContentMentions", () => { false, // showRooIgnoredFiles should default to false true, // includeDiagnosticMessages 50, // maxDiagnosticMessages + undefined, // maxReadFileLine ) }) @@ -251,6 +252,7 @@ describe("processUserContentMentions", () => { false, true, // includeDiagnosticMessages 50, // maxDiagnosticMessages + undefined, // maxReadFileLine ) }) }) diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index faa7236e67c..964e1fc4307 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -105,7 +105,8 @@ export interface ParseMentionsResult { * Formats file content to look like a read_file tool result. * Includes Gemini-style truncation warning when content is truncated. */ -function formatFileReadResult(filePath: string, result: ExtractTextResult): string { +function formatFileReadResult(filePath: string, result: ExtractTextResult, maxReadFileLine?: number): string { + const effectiveLimit = maxReadFileLine ?? DEFAULT_LINE_LIMIT const header = `[read_file for '${filePath}']` if (result.wasTruncated && result.linesShown) { @@ -114,7 +115,7 @@ function formatFileReadResult(filePath: string, result: ExtractTextResult): stri return `${header} IMPORTANT: File content truncated. Status: Showing lines ${start}-${end} of ${result.totalLines} total lines. -To read more: Use the read_file tool with offset=${nextOffset} and limit=${DEFAULT_LINE_LIMIT}. +To read more: Use the read_file tool with offset=${nextOffset} and limit=${effectiveLimit}. File: ${filePath} ${result.content}` @@ -134,6 +135,7 @@ export async function parseMentions( showRooIgnoredFiles: boolean = false, includeDiagnosticMessages: boolean = true, maxDiagnosticMessages: number = 50, + maxReadFileLine?: number, ): Promise { const mentions: Set = new Set() const validCommands: Map = new Map() @@ -249,6 +251,7 @@ export async function parseMentions( rooIgnoreController, showRooIgnoredFiles, fileContextTracker, + maxReadFileLine, ) contentBlocks.push(fileResult) } catch (error) { @@ -331,6 +334,7 @@ async function getFileOrFolderContentWithMetadata( rooIgnoreController?: any, showRooIgnoredFiles: boolean = false, fileContextTracker?: FileContextTracker, + maxReadFileLine?: number, ): Promise { const unescapedPath = unescapeSpaces(mentionPath) const absPath = path.resolve(cwd, unescapedPath) @@ -358,7 +362,8 @@ async function getFileOrFolderContentWithMetadata( } } try { - const result = await extractTextFromFileWithMetadata(absPath) + const effectiveLimit = maxReadFileLine ?? DEFAULT_LINE_LIMIT + const result = await extractTextFromFileWithMetadata(absPath, effectiveLimit) // Track file context if (fileContextTracker) { @@ -368,7 +373,7 @@ async function getFileOrFolderContentWithMetadata( return { type: "file", path: mentionPath, - content: formatFileReadResult(mentionPath, result), + content: formatFileReadResult(mentionPath, result, maxReadFileLine), metadata: { totalLines: result.totalLines, returnedLines: result.returnedLines, @@ -415,8 +420,12 @@ async function getFileOrFolderContentWithMetadata( try { const isBinary = await isBinaryFile(absoluteFilePath).catch(() => false) if (!isBinary) { - const result = await extractTextFromFileWithMetadata(absoluteFilePath) - fileReadResults.push(formatFileReadResult(filePath.toPosix(), result)) + const effectiveFolderLimit = maxReadFileLine ?? DEFAULT_LINE_LIMIT + const result = await extractTextFromFileWithMetadata( + absoluteFilePath, + effectiveFolderLimit, + ) + fileReadResults.push(formatFileReadResult(filePath.toPosix(), result, maxReadFileLine)) } } catch (error) { // Skip files that can't be read diff --git a/src/core/mentions/processUserContentMentions.ts b/src/core/mentions/processUserContentMentions.ts index d27f2cae66a..96e52845ff2 100644 --- a/src/core/mentions/processUserContentMentions.ts +++ b/src/core/mentions/processUserContentMentions.ts @@ -36,6 +36,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles = false, includeDiagnosticMessages = true, maxDiagnosticMessages = 50, + maxReadFileLine, }: { userContent: Anthropic.Messages.ContentBlockParam[] cwd: string @@ -45,6 +46,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles?: boolean includeDiagnosticMessages?: boolean maxDiagnosticMessages?: number + maxReadFileLine?: number }): Promise { // Track the first mode found from slash commands let commandMode: string | undefined @@ -72,6 +74,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine, ) // Capture the first mode found if (!commandMode && result.mode) { @@ -116,6 +119,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine, ) // Capture the first mode found if (!commandMode && result.mode) { @@ -166,6 +170,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine, ) // Capture the first mode found if (!commandMode && result.mode) { diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index 48f1071e1be..e4751e46034 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -32,6 +32,8 @@ export type { ReadFileToolOptions } from "./read_file" export interface NativeToolsOptions { /** Whether the model supports image processing (default: false) */ supportsImages?: boolean + /** Provider-specific override for the maximum lines returned per read */ + maxReadFileLine?: number } /** @@ -41,10 +43,11 @@ export interface NativeToolsOptions { * @returns Array of native tool definitions */ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.ChatCompletionTool[] { - const { supportsImages = false } = options + const { supportsImages = false, maxReadFileLine } = options const readFileOptions: ReadFileToolOptions = { supportsImages, + maxReadFileLine, } return [ diff --git a/src/core/prompts/tools/native-tools/read_file.ts b/src/core/prompts/tools/native-tools/read_file.ts index af781556ef6..d492465d8f2 100644 --- a/src/core/prompts/tools/native-tools/read_file.ts +++ b/src/core/prompts/tools/native-tools/read_file.ts @@ -34,6 +34,8 @@ function getReadFileSupportsNote(supportsImages: boolean): string { export interface ReadFileToolOptions { /** Whether the model supports image processing (default: false) */ supportsImages?: boolean + /** Provider-specific override for the maximum lines returned per read (default: DEFAULT_LINE_LIMIT) */ + maxReadFileLine?: number } // ─── Schema Builder ─────────────────────────────────────────────────────────── @@ -58,7 +60,10 @@ export interface ReadFileToolOptions { * @returns Native tool definition for read_file */ export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Chat.ChatCompletionTool { - const { supportsImages = false } = options + const { supportsImages = false, maxReadFileLine } = options + + // Compute the effective line limit for the tool description + const effectiveLineLimit = maxReadFileLine ?? DEFAULT_LINE_LIMIT // Build description based on capabilities const descriptionIntro = @@ -70,7 +75,7 @@ export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Ch ` PREFER indentation mode when you have a specific line number from search results, error messages, or definition lookups - it guarantees complete, syntactically valid code blocks without mid-function truncation.` + ` IMPORTANT: Indentation mode requires anchor_line to be useful. Without it, only header content (imports) is returned.` - const limitNote = ` By default, returns up to ${DEFAULT_LINE_LIMIT} lines per file. Lines longer than ${MAX_LINE_LENGTH} characters are truncated.` + const limitNote = ` By default, returns up to ${effectiveLineLimit} lines per file. Lines longer than ${MAX_LINE_LENGTH} characters are truncated.` const description = descriptionIntro + @@ -125,7 +130,7 @@ export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Ch }, limit: { type: "integer", - description: `Maximum number of lines to return (slice mode, default: ${DEFAULT_LINE_LIMIT})`, + description: `Maximum number of lines to return (slice mode, default: ${effectiveLineLimit})`, }, indentation: { type: "object", diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 9d27c4b90b0..74600c66e12 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -2800,6 +2800,7 @@ export class Task extends EventEmitter implements TaskLike { showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine: this.apiConfiguration?.maxReadFileLine, }) // Switch mode if specified in a slash command's frontmatter diff --git a/src/core/task/build-tools.ts b/src/core/task/build-tools.ts index ab74f9443ca..28c20cda863 100644 --- a/src/core/task/build-tools.ts +++ b/src/core/task/build-tools.ts @@ -114,6 +114,7 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO // Build native tools with dynamic read_file tool based on settings. const nativeTools = getNativeTools({ supportsImages, + maxReadFileLine: apiConfiguration?.maxReadFileLine, }) // Filter native tools based on mode restrictions. diff --git a/src/core/tools/ReadFileTool.ts b/src/core/tools/ReadFileTool.ts index 8ad6a3b33d1..59eb17bfd23 100644 --- a/src/core/tools/ReadFileTool.ts +++ b/src/core/tools/ReadFileTool.ts @@ -216,7 +216,8 @@ export class ReadFileTool extends BaseTool<"read_file"> { // (they become U+FFFD replacement characters instead of throwing) const buffer = await fs.readFile(fullPath) const fileContent = buffer.toString("utf-8") - const result = this.processTextFile(fileContent, entry) + const providerMaxReadFileLine = task.apiConfiguration?.maxReadFileLine + const result = this.processTextFile(fileContent, entry, providerMaxReadFileLine) await task.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) @@ -265,20 +266,30 @@ export class ReadFileTool extends BaseTool<"read_file"> { /** * Process a text file according to the requested mode. + * + * @param content - The raw file content + * @param entry - The parsed file entry parameters + * @param providerMaxReadFileLine - Optional provider-level cap on returned lines */ - private processTextFile(content: string, entry: InternalFileEntry): string { + private processTextFile(content: string, entry: InternalFileEntry, providerMaxReadFileLine?: number): string { const mode = entry.mode || "slice" + const defaultLimit = providerMaxReadFileLine ?? DEFAULT_LINE_LIMIT if (mode === "indentation") { // Indentation mode: semantic block extraction // When anchor_line is not provided, default to offset (which defaults to 1) const anchorLine = entry.anchor_line ?? entry.offset ?? 1 + // Clamp the limit: if the provider has a max, enforce it even when the model requests more + const requestedLimit = entry.limit ?? defaultLimit + const effectiveLimit = providerMaxReadFileLine + ? Math.min(requestedLimit, providerMaxReadFileLine) + : requestedLimit const result = readWithIndentation(content, { anchorLine, maxLevels: entry.max_levels, includeSiblings: entry.include_siblings, includeHeader: entry.include_header, - limit: entry.limit ?? DEFAULT_LINE_LIMIT, + limit: effectiveLimit, maxLines: entry.max_lines, }) @@ -287,7 +298,6 @@ export class ReadFileTool extends BaseTool<"read_file"> { if (result.wasTruncated && result.includedRanges.length > 0) { const [start, end] = result.includedRanges[0] const nextOffset = end + 1 - const effectiveLimit = entry.limit ?? DEFAULT_LINE_LIMIT // Put truncation warning at TOP (before content) to match @ mention format output = `IMPORTANT: File content truncated. Status: Showing lines ${start}-${end} of ${result.totalLines} total lines. @@ -306,7 +316,11 @@ export class ReadFileTool extends BaseTool<"read_file"> { // NOTE: read_file offset is 1-based externally; convert to 0-based for readWithSlice. const offset1 = entry.offset ?? 1 const offset0 = Math.max(0, offset1 - 1) - const limit = entry.limit ?? DEFAULT_LINE_LIMIT + // Clamp the limit: if the provider has a max, enforce it even when the model requests more + const requestedSliceLimit = entry.limit ?? defaultLimit + const limit = providerMaxReadFileLine + ? Math.min(requestedSliceLimit, providerMaxReadFileLine) + : requestedSliceLimit const result = readWithSlice(content, offset0, limit) @@ -786,8 +800,10 @@ export class ReadFileTool extends BaseTool<"read_file"> { } content = selectedLines.join("\n") } else { - // Read with default limits using slice mode - const result = readWithSlice(rawContent, 0, DEFAULT_LINE_LIMIT) + // Read with default limits using slice mode, clamped by provider setting + const providerMaxReadFileLine = task.apiConfiguration?.maxReadFileLine + const legacyLimit = providerMaxReadFileLine ?? DEFAULT_LINE_LIMIT + const result = readWithSlice(rawContent, 0, legacyLimit) content = result.content if (result.wasTruncated) { content += `\n\n[File truncated: showing ${result.returnedLines} of ${result.totalLines} total lines]` diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 51210de4f4a..27b44e447d4 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -102,6 +102,7 @@ import { Verbosity } from "./Verbosity" import { TodoListSettingsControl } from "./TodoListSettingsControl" import { TemperatureControl } from "./TemperatureControl" import { RateLimitSecondsControl } from "./RateLimitSecondsControl" +import { MaxReadFileLineControl } from "./MaxReadFileLineControl" import { ConsecutiveMistakeLimitControl } from "./ConsecutiveMistakeLimitControl" import { BedrockCustomArn } from "./providers/BedrockCustomArn" import { RooBalanceDisplay } from "./providers/RooBalanceDisplay" @@ -786,6 +787,10 @@ const ApiOptions = ({ value={apiConfiguration.rateLimitSeconds || 0} onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)} /> + setApiConfigurationField("maxReadFileLine", value)} + /> void +} + +export const MaxReadFileLineControl = ({ value, onChange }: MaxReadFileLineControlProps) => { + const { t } = useAppTranslation() + + return ( +
+ +
+ { + const raw = e.target.value + if (raw === "") { + onChange(undefined) + return + } + const newValue = parseInt(raw, 10) + if (!isNaN(newValue) && newValue >= 1) { + onChange(newValue) + } + }} + /> + {t("settings:providers.maxReadFileLine.unit")} +
+
+ {t("settings:providers.maxReadFileLine.description")} +
+
+ ) +} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 9f727255205..aa3cc54421d 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -598,6 +598,11 @@ "label": "Rate limit", "description": "Minimum time between API requests." }, + "maxReadFileLine": { + "label": "Max file read lines", + "unit": "lines", + "description": "Maximum number of lines returned when reading a file. Lower values reduce prompt size and help slower providers avoid timeouts. Leave empty to use the default (2000)." + }, "consecutiveMistakeLimit": { "label": "Error & Repetition Limit", "description": "Number of consecutive errors or repeated actions before showing 'Roo is having trouble' dialog. Set to 0 to disable this safety mechanism (it will never trigger).",