Skip to content
Draft
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
3 changes: 3 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ describe("processUserContentMentions", () => {
false, // showRooIgnoredFiles should default to false
true, // includeDiagnosticMessages
50, // maxDiagnosticMessages
undefined, // maxReadFileLine
)
})

Expand Down Expand Up @@ -251,6 +252,7 @@ describe("processUserContentMentions", () => {
false,
true, // includeDiagnosticMessages
50, // maxDiagnosticMessages
undefined, // maxReadFileLine
)
})
})
Expand Down
21 changes: 15 additions & 6 deletions src/core/mentions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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}`
Expand All @@ -134,6 +135,7 @@ export async function parseMentions(
showRooIgnoredFiles: boolean = false,
includeDiagnosticMessages: boolean = true,
maxDiagnosticMessages: number = 50,
maxReadFileLine?: number,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseMentions now has 9 positional parameters, which makes call sites fragile and hard to read (the tests that had to add undefined, // maxReadFileLine illustrate the problem). Its caller processUserContentMentions already uses a destructured options object. Consider refactoring parseMentions to accept an options object too, which would make adding future parameters straightforward and eliminate positional-ordering mistakes.

Fix it with Roo Code or mention @roomote and request a fix.

): Promise<ParseMentionsResult> {
const mentions: Set<string> = new Set()
const validCommands: Map<string, Command> = new Map()
Expand Down Expand Up @@ -249,6 +251,7 @@ export async function parseMentions(
rooIgnoreController,
showRooIgnoredFiles,
fileContextTracker,
maxReadFileLine,
)
contentBlocks.push(fileResult)
} catch (error) {
Expand Down Expand Up @@ -331,6 +334,7 @@ async function getFileOrFolderContentWithMetadata(
rooIgnoreController?: any,
showRooIgnoredFiles: boolean = false,
fileContextTracker?: FileContextTracker,
maxReadFileLine?: number,
): Promise<MentionContentBlock> {
const unescapedPath = unescapeSpaces(mentionPath)
const absPath = path.resolve(cwd, unescapedPath)
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/core/mentions/processUserContentMentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export async function processUserContentMentions({
showRooIgnoredFiles = false,
includeDiagnosticMessages = true,
maxDiagnosticMessages = 50,
maxReadFileLine,
}: {
userContent: Anthropic.Messages.ContentBlockParam[]
cwd: string
Expand All @@ -45,6 +46,7 @@ export async function processUserContentMentions({
showRooIgnoredFiles?: boolean
includeDiagnosticMessages?: boolean
maxDiagnosticMessages?: number
maxReadFileLine?: number
}): Promise<ProcessUserContentMentionsResult> {
// Track the first mode found from slash commands
let commandMode: string | undefined
Expand Down Expand Up @@ -72,6 +74,7 @@ export async function processUserContentMentions({
showRooIgnoredFiles,
includeDiagnosticMessages,
maxDiagnosticMessages,
maxReadFileLine,
)
// Capture the first mode found
if (!commandMode && result.mode) {
Expand Down Expand Up @@ -116,6 +119,7 @@ export async function processUserContentMentions({
showRooIgnoredFiles,
includeDiagnosticMessages,
maxDiagnosticMessages,
maxReadFileLine,
)
// Capture the first mode found
if (!commandMode && result.mode) {
Expand Down Expand Up @@ -166,6 +170,7 @@ export async function processUserContentMentions({
showRooIgnoredFiles,
includeDiagnosticMessages,
maxDiagnosticMessages,
maxReadFileLine,
)
// Capture the first mode found
if (!commandMode && result.mode) {
Expand Down
5 changes: 4 additions & 1 deletion src/core/prompts/tools/native-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand All @@ -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 [
Expand Down
11 changes: 8 additions & 3 deletions src/core/prompts/tools/native-tools/read_file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ───────────────────────────────────────────────────────────
Expand All @@ -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 =
Expand All @@ -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 +
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2800,6 +2800,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
showRooIgnoredFiles,
includeDiagnosticMessages,
maxDiagnosticMessages,
maxReadFileLine: this.apiConfiguration?.maxReadFileLine,
})

// Switch mode if specified in a slash command's frontmatter
Expand Down
1 change: 1 addition & 0 deletions src/core/task/build-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 23 additions & 7 deletions src/core/tools/ReadFileTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Comment on lines +282 to +286
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clamping logic here (Math.min(requestedLimit, providerMaxReadFileLine)) is the core enforcement mechanism of this feature, but it has no test coverage. The test changes in this PR only add undefined to existing assertions to match the new parameter. There should be tests that verify: (1) when providerMaxReadFileLine=500 and the model requests limit=1000, the effective limit is 500, (2) when providerMaxReadFileLine is undefined, the existing DEFAULT_LINE_LIMIT behavior is preserved, and (3) the same clamping applies in both slice and indentation mode. Since processTextFile is private, these can be exercised through executeNew with a mocked task.apiConfiguration.

Fix it with Roo Code or mention @roomote and request a fix.

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,
})

Expand All @@ -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.
Expand All @@ -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)

Expand Down Expand Up @@ -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]`
Expand Down
5 changes: 5 additions & 0 deletions webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -786,6 +787,10 @@ const ApiOptions = ({
value={apiConfiguration.rateLimitSeconds || 0}
onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)}
/>
<MaxReadFileLineControl
value={apiConfiguration.maxReadFileLine}
onChange={(value) => setApiConfigurationField("maxReadFileLine", value)}
/>
<ConsecutiveMistakeLimitControl
value={
apiConfiguration.consecutiveMistakeLimit !== undefined
Expand Down
41 changes: 41 additions & 0 deletions webview-ui/src/components/settings/MaxReadFileLineControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useAppTranslation } from "@/i18n/TranslationContext"

interface MaxReadFileLineControlProps {
value: number | undefined
onChange: (value: number | undefined) => void
}

export const MaxReadFileLineControl = ({ value, onChange }: MaxReadFileLineControlProps) => {
const { t } = useAppTranslation()

return (
<div className="flex flex-col gap-1">
<label className="block font-medium mb-1">{t("settings:providers.maxReadFileLine.label")}</label>
<div className="flex items-center gap-2">
<input
type="number"
className="w-24 bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border px-2 py-1 rounded text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={value ?? ""}
min={1}
max={100000}
placeholder="2000"
onChange={(e) => {
const raw = e.target.value
if (raw === "") {
onChange(undefined)
return
}
const newValue = parseInt(raw, 10)
if (!isNaN(newValue) && newValue >= 1) {
onChange(newValue)
}
}}
/>
<span>{t("settings:providers.maxReadFileLine.unit")}</span>
</div>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.maxReadFileLine.description")}
</div>
</div>
)
}
5 changes: 5 additions & 0 deletions webview-ui/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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).",
Expand Down
Loading