Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 7e61847

Browse files
committed
refactor(read_file): Codex-inspired indentation mode with simplified API
- Replace multi-file read_file with single-file-per-call design - Add two reading modes: slice (default) and indentation - Implement bidirectional expansion algorithm for indentation mode - Add line truncation (500 chars) and limit (2000 lines default) - Remove legacy token-budget-based reading approach - Remove maxReadFileLine setting (replaced by limit parameter) - Add new IndentationParams and ReadFileParams types - Clean up stale FileEntry/LineRange types and helpers Known limitations: - Lines >500 chars are truncated (content lost) - No server-side max limit enforcement
1 parent b9cf163 commit 7e61847

47 files changed

Lines changed: 1894 additions & 4578 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/vscode-e2e/src/suite/tools/read-file.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ suite.skip("Roo Code read_file Tool", function () {
376376
}
377377
})
378378

379-
test("Should read file with line range", async function () {
379+
test("Should read file with slice offset/limit", async function () {
380380
const api = globalThis.api
381381
const messages: ClineMessage[] = []
382382
let taskCompleted = false
@@ -446,7 +446,7 @@ suite.skip("Roo Code read_file Tool", function () {
446446
alwaysAllowReadOnly: true,
447447
alwaysAllowReadOnlyOutsideWorkspace: true,
448448
},
449-
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.`,
449+
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.`,
450450
})
451451

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

458-
// Verify the tool returned the correct lines (when line range is used)
458+
// Verify the tool returned the correct lines (offset=2, limit=3 -> lines 2-4)
459459
if (toolResult && (toolResult as string).includes(" | ")) {
460-
// The result includes line numbers
461460
assert.ok(
462461
(toolResult as string).includes("2 | Line 2"),
463462
"Tool result should include line 2 with line number",

packages/cloud/src/__tests__/CloudSettingsService.parsing.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ describe("CloudSettingsService - Response Parsing", () => {
8181
version: 2,
8282
defaultSettings: {
8383
maxOpenTabsContext: 10,
84-
maxReadFileLine: 1000,
8584
},
8685
allowList: {
8786
allowAll: false,

packages/types/src/cloud.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ export const organizationDefaultSettingsSchema = globalSettingsSchema
9595
.pick({
9696
enableCheckpoints: true,
9797
maxOpenTabsContext: true,
98-
maxReadFileLine: true,
9998
maxWorkspaceFiles: true,
10099
showRooIgnoredFiles: true,
101100
terminalCommandDelay: true,
@@ -109,7 +108,6 @@ export const organizationDefaultSettingsSchema = globalSettingsSchema
109108
.merge(
110109
z.object({
111110
maxOpenTabsContext: z.number().int().nonnegative().optional(),
112-
maxReadFileLine: z.number().int().gte(-1).optional(),
113111
maxWorkspaceFiles: z.number().int().nonnegative().optional(),
114112
terminalCommandDelay: z.number().int().nonnegative().optional(),
115113
terminalOutputLineLimit: z.number().int().nonnegative().optional(),

packages/types/src/global-settings.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ export const globalSettingsSchema = z.object({
143143
maxWorkspaceFiles: z.number().optional(),
144144
showRooIgnoredFiles: z.boolean().optional(),
145145
enableSubfolderRules: z.boolean().optional(),
146-
maxReadFileLine: z.number().optional(),
147146
maxImageFileSize: z.number().optional(),
148147
maxTotalImageSize: z.number().optional(),
149148

@@ -359,7 +358,6 @@ export const EVALS_SETTINGS: RooCodeSettings = {
359358
maxWorkspaceFiles: 200,
360359
maxGitStatusFiles: 20,
361360
showRooIgnoredFiles: true,
362-
maxReadFileLine: -1, // -1 to enable full file reading.
363361

364362
includeDiagnosticMessages: true,
365363
maxDiagnosticMessages: 50,

packages/types/src/tool-params.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,45 @@
22
* Tool parameter type definitions for native protocol
33
*/
44

5-
export interface LineRange {
6-
start: number
7-
end: number
5+
/**
6+
* Read mode for the read_file tool.
7+
* - "slice": Simple offset/limit reading (default)
8+
* - "indentation": Semantic block extraction based on code structure
9+
*/
10+
export type ReadFileMode = "slice" | "indentation"
11+
12+
/**
13+
* Indentation-mode configuration for the read_file tool.
14+
*/
15+
export interface IndentationParams {
16+
/** 1-based line number to anchor indentation extraction (defaults to offset) */
17+
anchor_line?: number
18+
/** Maximum indentation levels to include above anchor (0 = unlimited) */
19+
max_levels?: number
20+
/** Include sibling blocks at the same indentation level */
21+
include_siblings?: boolean
22+
/** Include file header (imports, comments at top) */
23+
include_header?: boolean
24+
/** Hard cap on lines returned for indentation mode */
25+
max_lines?: number
826
}
927

10-
export interface FileEntry {
28+
/**
29+
* Parameters for the read_file tool.
30+
*
31+
* NOTE: This is the canonical, single-file-per-call shape.
32+
*/
33+
export interface ReadFileParams {
34+
/** Path to the file, relative to workspace */
1135
path: string
12-
lineRanges?: LineRange[]
36+
/** Reading mode: "slice" (default) or "indentation" */
37+
mode?: ReadFileMode
38+
/** 1-based line number to start reading from (slice mode, default: 1) */
39+
offset?: number
40+
/** Maximum number of lines to read (default: 2000) */
41+
limit?: number
42+
/** Indentation-mode configuration (only used when mode === "indentation") */
43+
indentation?: IndentationParams
1344
}
1445

1546
export interface Coordinate {

packages/types/src/vscode-extension-host.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ export interface ExtensionMessage {
6464
| "remoteBrowserEnabled"
6565
| "ttsStart"
6666
| "ttsStop"
67-
| "maxReadFileLine"
6867
| "fileSearchResults"
6968
| "toggleApiConfigPin"
7069
| "acceptInput"
@@ -354,7 +353,6 @@ export type ExtensionState = Pick<
354353
maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500)
355354
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
356355
enableSubfolderRules: boolean // Whether to load rules from subdirectories
357-
maxReadFileLine: number // Maximum number of lines to read from a file before truncating
358356
maxImageFileSize: number // Maximum size of image files to process in MB
359357
maxTotalImageSize: number // Maximum total size for all images in a single read operation in MB
360358

src/__tests__/command-mentions.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ describe("Command Mentions", () => {
3636
false, // showRooIgnoredFiles
3737
true, // includeDiagnosticMessages
3838
50, // maxDiagnosticMessages
39-
undefined, // maxReadFileLine
4039
)
4140
}
4241

src/api/providers/__tests__/bedrock-native-tools.spec.ts

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -135,23 +135,18 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
135135
parameters: {
136136
type: "object",
137137
properties: {
138-
files: {
139-
type: "array",
140-
items: {
141-
type: "object",
142-
properties: {
143-
path: { type: "string" },
144-
line_ranges: {
145-
type: ["array", "null"],
146-
items: { type: "integer" },
147-
description: "Optional line ranges",
148-
},
138+
path: { type: "string" },
139+
indentation: {
140+
type: ["object", "null"],
141+
properties: {
142+
anchor_line: {
143+
type: ["integer", "null"],
144+
description: "Optional anchor line",
149145
},
150-
required: ["path", "line_ranges"],
151146
},
152147
},
153148
},
154-
required: ["files"],
149+
required: ["path"],
155150
},
156151
},
157152
},
@@ -167,15 +162,14 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
167162
expect(executeCommandSchema.properties.cwd.type).toBeUndefined()
168163
expect(executeCommandSchema.properties.cwd.description).toBe("Working directory (optional)")
169164

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

181175
it("should filter non-function tools", () => {

src/core/assistant-message/NativeToolCallParser.ts

Lines changed: 49 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { parseJSON } from "partial-json"
22

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

66
import {
@@ -313,43 +313,17 @@ export class NativeToolCallParser {
313313
return finalToolUse
314314
}
315315

316-
/**
317-
* Convert raw file entries from API (with line_ranges) to FileEntry objects
318-
* (with lineRanges). Handles multiple formats for compatibility:
319-
*
320-
* New tuple format: { path: string, line_ranges: [[1, 50], [100, 150]] }
321-
* Object format: { path: string, line_ranges: [{ start: 1, end: 50 }] }
322-
* Legacy string format: { path: string, line_ranges: ["1-50"] }
323-
*
324-
* Returns: { path: string, lineRanges: [{ start: 1, end: 50 }] }
325-
*/
326-
private static convertFileEntries(files: any[]): FileEntry[] {
327-
return files.map((file: any) => {
328-
const entry: FileEntry = { path: file.path }
329-
if (file.line_ranges && Array.isArray(file.line_ranges)) {
330-
entry.lineRanges = file.line_ranges
331-
.map((range: any) => {
332-
// Handle tuple format: [start, end]
333-
if (Array.isArray(range) && range.length >= 2) {
334-
return { start: Number(range[0]), end: Number(range[1]) }
335-
}
336-
// Handle object format: { start: number, end: number }
337-
if (typeof range === "object" && range !== null && "start" in range && "end" in range) {
338-
return { start: Number(range.start), end: Number(range.end) }
339-
}
340-
// Handle legacy string format: "1-50"
341-
if (typeof range === "string") {
342-
const match = range.match(/^(\d+)-(\d+)$/)
343-
if (match) {
344-
return { start: parseInt(match[1], 10), end: parseInt(match[2], 10) }
345-
}
346-
}
347-
return null
348-
})
349-
.filter(Boolean)
316+
private static coerceOptionalNumber(value: unknown): number | undefined {
317+
if (typeof value === "number" && Number.isFinite(value)) {
318+
return value
319+
}
320+
if (typeof value === "string") {
321+
const n = Number(value)
322+
if (Number.isFinite(n)) {
323+
return n
350324
}
351-
return entry
352-
})
325+
}
326+
return undefined
353327
}
354328

355329
/**
@@ -380,8 +354,26 @@ export class NativeToolCallParser {
380354

381355
switch (name) {
382356
case "read_file":
383-
if (partialArgs.files && Array.isArray(partialArgs.files)) {
384-
nativeArgs = { files: this.convertFileEntries(partialArgs.files) }
357+
if (partialArgs.path !== undefined) {
358+
nativeArgs = {
359+
path: partialArgs.path,
360+
mode: partialArgs.mode,
361+
offset: this.coerceOptionalNumber(partialArgs.offset),
362+
limit: this.coerceOptionalNumber(partialArgs.limit),
363+
indentation:
364+
partialArgs.indentation && typeof partialArgs.indentation === "object"
365+
? {
366+
anchor_line: this.coerceOptionalNumber(partialArgs.indentation.anchor_line),
367+
max_levels: this.coerceOptionalNumber(partialArgs.indentation.max_levels),
368+
include_siblings: this.coerceOptionalBoolean(
369+
partialArgs.indentation.include_siblings,
370+
),
371+
include_header: this.coerceOptionalBoolean(
372+
partialArgs.indentation.include_header,
373+
),
374+
}
375+
: undefined,
376+
}
385377
}
386378
break
387379

@@ -641,13 +633,6 @@ export class NativeToolCallParser {
641633
const params: Partial<Record<ToolParamName, string>> = {}
642634

643635
for (const [key, value] of Object.entries(args)) {
644-
// Skip complex parameters that have been migrated to nativeArgs.
645-
// For read_file, the 'files' parameter is a FileEntry[] array that can't be
646-
// meaningfully stringified. The properly typed data is in nativeArgs instead.
647-
if (resolvedName === "read_file" && key === "files") {
648-
continue
649-
}
650-
651636
// Validate parameter name
652637
if (!toolParamNames.includes(key as ToolParamName) && !customToolRegistry.has(resolvedName)) {
653638
console.warn(`Unknown parameter '${key}' for tool '${resolvedName}'`)
@@ -667,8 +652,24 @@ export class NativeToolCallParser {
667652

668653
switch (resolvedName) {
669654
case "read_file":
670-
if (args.files && Array.isArray(args.files)) {
671-
nativeArgs = { files: this.convertFileEntries(args.files) } as NativeArgsFor<TName>
655+
if (args.path !== undefined) {
656+
nativeArgs = {
657+
path: args.path,
658+
mode: args.mode,
659+
offset: this.coerceOptionalNumber(args.offset),
660+
limit: this.coerceOptionalNumber(args.limit),
661+
indentation:
662+
args.indentation && typeof args.indentation === "object"
663+
? {
664+
anchor_line: this.coerceOptionalNumber(args.indentation.anchor_line),
665+
max_levels: this.coerceOptionalNumber(args.indentation.max_levels),
666+
include_siblings: this.coerceOptionalBoolean(
667+
args.indentation.include_siblings,
668+
),
669+
include_header: this.coerceOptionalBoolean(args.indentation.include_header),
670+
}
671+
: undefined,
672+
} as NativeArgsFor<TName>
672673
}
673674
break
674675

0 commit comments

Comments
 (0)