Skip to content
Open
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
7 changes: 3 additions & 4 deletions apps/vscode-e2e/src/suite/tools/read-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ suite.skip("Roo Code read_file Tool", function () {
}
})

test("Should read file with line range", async function () {
test("Should read file with slice offset/limit", async function () {
const api = globalThis.api
const messages: ClineMessage[] = []
let taskCompleted = false
Expand Down Expand Up @@ -446,7 +446,7 @@ suite.skip("Roo Code read_file Tool", function () {
alwaysAllowReadOnly: true,
alwaysAllowReadOnlyOutsideWorkspace: true,
},
text: `Use the read_file tool to read the file "${fileName}" and show me what's on lines 2, 3, and 4. The file contains lines like "Line 1", "Line 2", etc. Assume the file exists and you can read it directly.`,
text: `Use the read_file tool to read the file "${fileName}" using slice mode with offset=2 and limit=3 (1-based offset). The file contains lines like "Line 1", "Line 2", etc. After reading, show me the three lines you read.`,
})

// Wait for task completion
Expand All @@ -455,9 +455,8 @@ suite.skip("Roo Code read_file Tool", function () {
// Verify tool was executed
assert.ok(toolExecuted, "The read_file tool should have been executed")

// Verify the tool returned the correct lines (when line range is used)
// Verify the tool returned the correct lines (offset=2, limit=3 -> lines 2-4)
if (toolResult && (toolResult as string).includes(" | ")) {
// The result includes line numbers
assert.ok(
(toolResult as string).includes("2 | Line 2"),
"Tool result should include line 2 with line number",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ describe("CloudSettingsService - Response Parsing", () => {
version: 2,
defaultSettings: {
maxOpenTabsContext: 10,
maxReadFileLine: 1000,
},
allowList: {
allowAll: false,
Expand Down
2 changes: 0 additions & 2 deletions packages/types/src/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ export const organizationDefaultSettingsSchema = globalSettingsSchema
.pick({
enableCheckpoints: true,
maxOpenTabsContext: true,
maxReadFileLine: true,
maxWorkspaceFiles: true,
showRooIgnoredFiles: true,
terminalCommandDelay: true,
Expand All @@ -107,7 +106,6 @@ export const organizationDefaultSettingsSchema = globalSettingsSchema
.merge(
z.object({
maxOpenTabsContext: z.number().int().nonnegative().optional(),
maxReadFileLine: z.number().int().gte(-1).optional(),
maxWorkspaceFiles: z.number().int().nonnegative().optional(),
terminalCommandDelay: z.number().int().nonnegative().optional(),
terminalShellIntegrationTimeout: z.number().int().nonnegative().optional(),
Expand Down
3 changes: 0 additions & 3 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export const globalSettingsSchema = z.object({
allowedMaxCost: z.number().nullish(),
autoCondenseContext: z.boolean().optional(),
autoCondenseContextPercent: z.number().optional(),
maxConcurrentFileReads: z.number().optional(),

/**
* Whether to include current time in the environment details
Expand Down Expand Up @@ -172,7 +171,6 @@ export const globalSettingsSchema = z.object({
maxWorkspaceFiles: z.number().optional(),
showRooIgnoredFiles: z.boolean().optional(),
enableSubfolderRules: z.boolean().optional(),
maxReadFileLine: z.number().optional(),
maxImageFileSize: z.number().optional(),
maxTotalImageSize: z.number().optional(),

Expand Down Expand Up @@ -383,7 +381,6 @@ export const EVALS_SETTINGS: RooCodeSettings = {
maxWorkspaceFiles: 200,
maxGitStatusFiles: 20,
showRooIgnoredFiles: true,
maxReadFileLine: -1, // -1 to enable full file reading.

includeDiagnosticMessages: true,
maxDiagnosticMessages: 50,
Expand Down
41 changes: 36 additions & 5 deletions packages/types/src/tool-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,45 @@
* Tool parameter type definitions for native protocol
*/

export interface LineRange {
start: number
end: number
/**
* Read mode for the read_file tool.
* - "slice": Simple offset/limit reading (default)
* - "indentation": Semantic block extraction based on code structure
*/
export type ReadFileMode = "slice" | "indentation"

/**
* Indentation-mode configuration for the read_file tool.
*/
export interface IndentationParams {
/** 1-based line number to anchor indentation extraction (defaults to offset) */
anchor_line?: number
/** Maximum indentation levels to include above anchor (0 = unlimited) */
max_levels?: number
/** Include sibling blocks at the same indentation level */
include_siblings?: boolean
/** Include file header (imports, comments at top) */
include_header?: boolean
/** Hard cap on lines returned for indentation mode */
max_lines?: number
}

export interface FileEntry {
/**
* Parameters for the read_file tool.
*
* NOTE: This is the canonical, single-file-per-call shape.
*/
export interface ReadFileParams {
/** Path to the file, relative to workspace */
path: string
lineRanges?: LineRange[]
/** Reading mode: "slice" (default) or "indentation" */
mode?: ReadFileMode
/** 1-based line number to start reading from (slice mode, default: 1) */
offset?: number
/** Maximum number of lines to read (default: 2000) */
limit?: number
/** Indentation-mode configuration (only used when mode === "indentation") */
indentation?: IndentationParams
}

export interface Coordinate {
Expand Down
4 changes: 1 addition & 3 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ export interface ExtensionMessage {
| "remoteBrowserEnabled"
| "ttsStart"
| "ttsStop"
| "maxReadFileLine"
| "fileSearchResults"
| "toggleApiConfigPin"
| "acceptInput"
Expand Down Expand Up @@ -301,7 +300,6 @@ export type ExtensionState = Pick<
| "ttsSpeed"
| "soundEnabled"
| "soundVolume"
| "maxConcurrentFileReads"
| "terminalOutputPreviewSize"
| "terminalShellIntegrationTimeout"
| "terminalShellIntegrationDisabled"
Expand Down Expand Up @@ -352,7 +350,6 @@ export type ExtensionState = Pick<
maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500)
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
enableSubfolderRules: boolean // Whether to load rules from subdirectories
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

Expand Down Expand Up @@ -809,6 +806,7 @@ export interface ClineSayTool {
isProtected?: boolean
additionalFileCount?: number // Number of additional files in the same read_file request
lineNumber?: number
startLine?: number // Starting line for read_file operations (for navigation on click)
query?: string
batchFiles?: Array<{
path: string
Expand Down
1 change: 0 additions & 1 deletion src/__tests__/command-mentions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ describe("Command Mentions", () => {
false, // showRooIgnoredFiles
true, // includeDiagnosticMessages
50, // maxDiagnosticMessages
undefined, // maxReadFileLine
)
}

Expand Down
36 changes: 15 additions & 21 deletions src/api/providers/__tests__/bedrock-native-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,23 +135,18 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
parameters: {
type: "object",
properties: {
files: {
type: "array",
items: {
type: "object",
properties: {
path: { type: "string" },
line_ranges: {
type: ["array", "null"],
items: { type: "integer" },
description: "Optional line ranges",
},
path: { type: "string" },
indentation: {
type: ["object", "null"],
properties: {
anchor_line: {
type: ["integer", "null"],
description: "Optional anchor line",
},
required: ["path", "line_ranges"],
},
},
},
required: ["files"],
required: ["path"],
},
},
},
Expand All @@ -167,15 +162,14 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
expect(executeCommandSchema.properties.cwd.type).toBeUndefined()
expect(executeCommandSchema.properties.cwd.description).toBe("Working directory (optional)")

// Second tool: line_ranges should be transformed from type: ["array", "null"] to anyOf
// with items moved inside the array variant (required by GPT-5-mini strict schema validation)
// Second tool: nested nullable object should be transformed from type: ["object", "null"] to anyOf
const readFileSchema = bedrockTools[1].toolSpec.inputSchema.json as any
const lineRanges = readFileSchema.properties.files.items.properties.line_ranges
expect(lineRanges.anyOf).toEqual([{ type: "array", items: { type: "integer" } }, { type: "null" }])
expect(lineRanges.type).toBeUndefined()
// items should now be inside the array variant, not at root
expect(lineRanges.items).toBeUndefined()
expect(lineRanges.description).toBe("Optional line ranges")
const indentation = readFileSchema.properties.indentation
expect(indentation.anyOf).toBeDefined()
expect(indentation.type).toBeUndefined()
// Object-level schema properties are preserved at the root, not inside the anyOf object variant
expect(indentation.additionalProperties).toBe(false)
expect(indentation.properties.anchor_line.anyOf).toEqual([{ type: "integer" }, { type: "null" }])
})

it("should filter non-function tools", () => {
Expand Down
99 changes: 51 additions & 48 deletions src/core/assistant-message/NativeToolCallParser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { parseJSON } from "partial-json"

import { type ToolName, toolNames, type FileEntry } from "@roo-code/types"
import { type ToolName, toolNames } from "@roo-code/types"
import { customToolRegistry } from "@roo-code/core"

import {
Expand Down Expand Up @@ -313,43 +313,17 @@ export class NativeToolCallParser {
return finalToolUse
}

/**
* Convert raw file entries from API (with line_ranges) to FileEntry objects
* (with lineRanges). Handles multiple formats for compatibility:
*
* New tuple format: { path: string, line_ranges: [[1, 50], [100, 150]] }
* Object format: { path: string, line_ranges: [{ start: 1, end: 50 }] }
* Legacy string format: { path: string, line_ranges: ["1-50"] }
*
* Returns: { path: string, lineRanges: [{ start: 1, end: 50 }] }
*/
private static convertFileEntries(files: any[]): FileEntry[] {
return files.map((file: any) => {
const entry: FileEntry = { path: file.path }
if (file.line_ranges && Array.isArray(file.line_ranges)) {
entry.lineRanges = file.line_ranges
.map((range: any) => {
// Handle tuple format: [start, end]
if (Array.isArray(range) && range.length >= 2) {
return { start: Number(range[0]), end: Number(range[1]) }
}
// Handle object format: { start: number, end: number }
if (typeof range === "object" && range !== null && "start" in range && "end" in range) {
return { start: Number(range.start), end: Number(range.end) }
}
// Handle legacy string format: "1-50"
if (typeof range === "string") {
const match = range.match(/^(\d+)-(\d+)$/)
if (match) {
return { start: parseInt(match[1], 10), end: parseInt(match[2], 10) }
}
}
return null
})
.filter(Boolean)
private static coerceOptionalNumber(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) {
return value
}
if (typeof value === "string") {
const n = Number(value)
if (Number.isFinite(n)) {
return n
}
return entry
})
}
return undefined
}

/**
Expand Down Expand Up @@ -380,8 +354,27 @@ export class NativeToolCallParser {

switch (name) {
case "read_file":
if (partialArgs.files && Array.isArray(partialArgs.files)) {
nativeArgs = { files: this.convertFileEntries(partialArgs.files) }
if (partialArgs.path !== undefined) {
nativeArgs = {
path: partialArgs.path,
mode: partialArgs.mode,
offset: this.coerceOptionalNumber(partialArgs.offset),
limit: this.coerceOptionalNumber(partialArgs.limit),
indentation:
partialArgs.indentation && typeof partialArgs.indentation === "object"
? {
anchor_line: this.coerceOptionalNumber(partialArgs.indentation.anchor_line),
max_levels: this.coerceOptionalNumber(partialArgs.indentation.max_levels),
max_lines: this.coerceOptionalNumber(partialArgs.indentation.max_lines),
include_siblings: this.coerceOptionalBoolean(
partialArgs.indentation.include_siblings,
),
include_header: this.coerceOptionalBoolean(
partialArgs.indentation.include_header,
),
}
: undefined,
}
}
break

Expand Down Expand Up @@ -641,13 +634,6 @@ export class NativeToolCallParser {
const params: Partial<Record<ToolParamName, string>> = {}

for (const [key, value] of Object.entries(args)) {
// Skip complex parameters that have been migrated to nativeArgs.
// For read_file, the 'files' parameter is a FileEntry[] array that can't be
// meaningfully stringified. The properly typed data is in nativeArgs instead.
if (resolvedName === "read_file" && key === "files") {
continue
}

// Validate parameter name
if (!toolParamNames.includes(key as ToolParamName) && !customToolRegistry.has(resolvedName)) {
console.warn(`Unknown parameter '${key}' for tool '${resolvedName}'`)
Expand All @@ -667,8 +653,25 @@ export class NativeToolCallParser {

switch (resolvedName) {
case "read_file":
if (args.files && Array.isArray(args.files)) {
nativeArgs = { files: this.convertFileEntries(args.files) } as NativeArgsFor<TName>
if (args.path !== undefined) {
nativeArgs = {
path: args.path,
mode: args.mode,
offset: this.coerceOptionalNumber(args.offset),
limit: this.coerceOptionalNumber(args.limit),
indentation:
args.indentation && typeof args.indentation === "object"
? {
anchor_line: this.coerceOptionalNumber(args.indentation.anchor_line),
max_levels: this.coerceOptionalNumber(args.indentation.max_levels),
max_lines: this.coerceOptionalNumber(args.indentation.max_lines),
include_siblings: this.coerceOptionalBoolean(
args.indentation.include_siblings,
),
include_header: this.coerceOptionalBoolean(args.indentation.include_header),
}
: undefined,
} as NativeArgsFor<TName>
}
break

Expand Down
Loading
Loading