This commit is contained in:
Will Li 2025-07-28 15:40:28 -07:00
parent d62a260576
commit 5c48253148
4 changed files with 654 additions and 8 deletions

View file

@ -0,0 +1,380 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { validateFileSizeForContext } from "../contextValidator"
import { Task } from "../../task/Task"
import { promises as fs } from "fs"
import { readLines } from "../../../integrations/misc/read-lines"
import * as sharedApi from "../../../shared/api"
vi.mock("fs", () => ({
promises: {
stat: vi.fn(),
},
}))
vi.mock("../../../integrations/misc/read-lines", () => ({
readLines: vi.fn(),
}))
vi.mock("../../../shared/api", () => ({
getModelMaxOutputTokens: vi.fn(),
}))
describe("contextValidator", () => {
let mockTask: Task
beforeEach(() => {
vi.clearAllMocks()
// Mock Task instance
mockTask = {
api: {
getModel: vi.fn().mockReturnValue({
id: "test-model",
info: {
contextWindow: 100000,
maxTokens: 4096,
},
}),
countTokens: vi.fn().mockResolvedValue(1000),
},
getTokenUsage: vi.fn().mockReturnValue({
contextTokens: 10000,
}),
apiConfiguration: {
apiProvider: "anthropic",
},
providerRef: {
deref: vi.fn().mockReturnValue({
getState: vi.fn().mockResolvedValue({}),
}),
},
} as any
// Mock getModelMaxOutputTokens to return a consistent value
vi.mocked(sharedApi.getModelMaxOutputTokens).mockReturnValue(4096)
})
describe("validateFileSizeForContext", () => {
it("should apply 25% buffer to remaining context and read incrementally", async () => {
const mockStats = { size: 50000 }
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
// Mock readLines to return content in batches
// Each batch is 100 lines, returning content that results in 1200 tokens per batch
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
const start = startLine ?? 0
const end = endLine ?? 99
const lines = end - start + 1
return `test content line\n`.repeat(lines)
})
// Mock token count - 12 tokens per line (1200 per 100-line batch)
let callCount = 0
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
callCount++
const text = content[0].text
const lines = text.split("\n").length - 1
return lines * 12 // 12 tokens per line
})
const result = await validateFileSizeForContext(
"/test/file.ts",
1000, // totalLines
-1, // currentMaxReadFileLine
mockTask,
)
// New calculation:
// Context window = 100k, current usage = 10k
// Remaining = 90k
// With 25% buffer on remaining: usable = 90k * 0.75 = 67.5k
// Reserved for response ~2k
// Available should be around 65.5k tokens
// File needs 12k tokens total (1000 lines * 12 tokens)
expect(result.shouldLimit).toBe(false)
// Verify readLines was called multiple times (incremental reading)
expect(readLines).toHaveBeenCalled()
// Verify the new calculation approach
const remaining = 100000 - 10000 // 90k remaining
const usableRemaining = remaining * 0.75 // 67.5k with 25% buffer
expect(usableRemaining).toBe(67500)
})
it("should handle different context usage levels correctly", async () => {
const mockStats = { size: 50000 }
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
// Mock readLines
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
const start = startLine ?? 0
const end = endLine ?? 99
const lines = end - start + 1
return `test content line\n`.repeat(lines)
})
// Mock token count - 50 tokens per line
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
const text = content[0].text
const lines = text.split("\n").length - 1
return lines * 50
})
// Test with 50% context already used
mockTask.getTokenUsage = vi.fn().mockReturnValue({
contextTokens: 50000, // 50% of 100k context used
})
const result = await validateFileSizeForContext(
"/test/file.ts",
2000, // totalLines
-1,
mockTask,
)
// With 50k remaining and 25% buffer: 50k * 0.75 = 37.5k usable
// Minus ~2k for response = ~35.5k available
// File needs 100k tokens (2000 lines * 50 tokens)
// Should limit the file
expect(result.shouldLimit).toBe(true)
expect(result.safeMaxLines).toBeLessThan(2000)
expect(result.reason).toContain("exceeds available context space")
})
it("should limit file when it exceeds available space with buffer", async () => {
// Set up a scenario where file is too large
const mockStats = { size: 500000 } // Large file
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
// Mock readLines to return content in batches
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
const start = startLine ?? 0
const end = endLine ?? 99
const lines = end - start + 1
return `large content line\n`.repeat(lines)
})
// Mock large token count - 100 tokens per line
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
const text = content[0].text
const lines = text.split("\n").length - 1
return lines * 100 // 100 tokens per line
})
const result = await validateFileSizeForContext(
"/test/largefile.ts",
10000, // totalLines
-1,
mockTask,
)
expect(result.shouldLimit).toBe(true)
expect(result.safeMaxLines).toBeGreaterThan(0)
expect(result.safeMaxLines).toBeLessThan(10000) // Should stop before reading all lines
expect(result.reason).toContain("exceeds available context space")
})
it("should handle very large files through incremental reading", async () => {
// Set up a file larger than 50MB
const mockStats = { size: 60_000_000 } // 60MB file
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
// Mock readLines to return content in batches
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
const start = startLine ?? 0
const end = endLine ?? 99
const lines = end - start + 1
return `large file content line\n`.repeat(lines)
})
// Mock very high token count per line (simulating dense content)
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
const text = content[0].text
const lines = text.split("\n").length - 1
return lines * 200 // 200 tokens per line for very large file
})
const result = await validateFileSizeForContext(
"/test/hugefile.ts",
100000, // totalLines
-1,
mockTask,
)
expect(result.shouldLimit).toBe(true)
// Should have attempted to read the file incrementally
expect(readLines).toHaveBeenCalled()
// Should stop early due to token limits
expect(result.safeMaxLines).toBeLessThan(1000)
expect(result.reason).toContain("exceeds available context space")
})
it("should handle read failures gracefully", async () => {
const mockStats = { size: 100000 } // 100KB file
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
// Mock readLines to fail
vi.mocked(readLines).mockRejectedValue(new Error("Read error"))
const result = await validateFileSizeForContext(
"/test/problematic.ts",
2000, // totalLines
-1,
mockTask,
)
// Should return a safe default when reading fails
expect(result.shouldLimit).toBe(true)
expect(result.safeMaxLines).toBe(50) // Minimum useful lines
})
it("should handle very limited context space", async () => {
const mockStats = { size: 10000 } // 10KB file
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
// Set very high context usage
// With new calculation: 100k - 95k = 5k remaining
// 5k * 0.75 = 3.75k usable
// Minus ~2k for response = ~1.75k available
mockTask.getTokenUsage = vi.fn().mockReturnValue({
contextTokens: 95000, // 95% of context used
})
// Mock small token count
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
const text = content[0].text
const lines = text.split("\n").length - 1
return lines * 10 // 10 tokens per line
})
// Mock readLines
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
const start = startLine ?? 0
const end = endLine ?? 99
const lines = end - start + 1
return `test line\n`.repeat(lines)
})
const result = await validateFileSizeForContext(
"/test/smallfile.ts",
500, // totalLines
-1,
mockTask,
)
expect(result.shouldLimit).toBe(true)
// With the new calculation using full model max tokens (4096),
// we have less space available, so we get the minimum 50 lines
expect(result.safeMaxLines).toBe(50)
expect(result.reason).toContain("Very limited context space")
})
it("should handle negative available space gracefully", async () => {
const mockStats = { size: 10000 } // 10KB file
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
// Set extremely high context usage
// With 100k - 99k = 1k remaining
// 1k * 0.75 = 750 tokens usable
// Minus 2k for response = negative available space
mockTask.getTokenUsage = vi.fn().mockReturnValue({
contextTokens: 99000, // 99% of context used
})
const result = await validateFileSizeForContext(
"/test/smallfile.ts",
500, // totalLines
-1,
mockTask,
)
expect(result.shouldLimit).toBe(true)
expect(result.safeMaxLines).toBe(50) // Should be limited to minimum useful lines
expect(result.reason).toContain("Very limited context space")
// With negative available space, readLines won't be called
expect(readLines).not.toHaveBeenCalled()
})
it("should limit file when it is too large and would be truncated", async () => {
const filePath = "/test/large-file.ts"
const totalLines = 10000
const currentMaxReadFileLine = -1 // Unlimited
// Set up context to have limited space
mockTask.getTokenUsage = vi.fn().mockReturnValue({
contextTokens: 90000, // 90% of context used
})
// Mock token counting to simulate a large file
mockTask.api.countTokens = vi.fn().mockResolvedValue(1000) // Each batch is 1000 tokens
// Mock readLines to return some content
vi.mocked(readLines).mockResolvedValue("line content")
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
expect(result.shouldLimit).toBe(true)
expect(result.safeMaxLines).toBeGreaterThan(0)
expect(result.safeMaxLines).toBeLessThan(totalLines)
expect(result.reason).toContain("File exceeds available context space")
expect(result.reason).toContain("Use line_range to read specific sections")
})
it("should limit file when very limited context space", async () => {
const filePath = "/test/file.ts"
const totalLines = 1000
const currentMaxReadFileLine = -1
// Mock very high token usage leaving little room
mockTask.getTokenUsage = vi.fn().mockReturnValue({
contextTokens: 98000, // Almost all context used (98% of 100k)
})
// Mock token counting to quickly exceed limit
mockTask.api.countTokens = vi.fn().mockResolvedValue(500) // Each batch uses a lot of tokens
vi.mocked(readLines).mockResolvedValue("line content")
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
expect(result.shouldLimit).toBe(true)
expect(result.reason).toContain("Very limited context space")
expect(result.reason).toContain("Consider using search_files or line_range")
})
it("should not limit when file fits within context", async () => {
const filePath = "/test/small-file.ts"
const totalLines = 100
const currentMaxReadFileLine = -1
// Mock low token usage
mockTask.api.countTokens = vi.fn().mockResolvedValue(10) // Small token count per batch
vi.mocked(readLines).mockResolvedValue("line content")
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
expect(result.shouldLimit).toBe(false)
expect(result.safeMaxLines).toBe(currentMaxReadFileLine)
})
it("should handle errors gracefully", async () => {
const filePath = "/test/error-file.ts"
const totalLines = 20000 // Large file
const currentMaxReadFileLine = -1
// Mock an error in the API
mockTask.api.getModel = vi.fn().mockImplementation(() => {
throw new Error("API Error")
})
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
// Should fall back to conservative limits
expect(result.shouldLimit).toBe(true)
expect(result.safeMaxLines).toBe(1000)
expect(result.reason).toContain("Large file detected")
})
})
})

View file

@ -4,12 +4,13 @@ import * as path from "path"
import { countFileLines } from "../../../integrations/misc/line-counter"
import { readLines } from "../../../integrations/misc/read-lines"
import { extractTextFromFile } from "../../../integrations/misc/extract-text"
import { extractTextFromFile, addLineNumbers } from "../../../integrations/misc/extract-text"
import { parseSourceCodeDefinitionsForFile } from "../../../services/tree-sitter"
import { isBinaryFile } from "isbinaryfile"
import { ReadFileToolUse, ToolParamName, ToolResponse } from "../../../shared/tools"
import { readFileTool } from "../readFileTool"
import { formatResponse } from "../../prompts/responses"
import * as contextValidatorModule from "../contextValidator"
vi.mock("path", async () => {
const originalPath = await vi.importActual("path")
@ -30,6 +31,7 @@ vi.mock("isbinaryfile")
vi.mock("../../../integrations/misc/line-counter")
vi.mock("../../../integrations/misc/read-lines")
vi.mock("../contextValidator")
// Mock input content for tests
let mockInputContent = ""
@ -90,6 +92,12 @@ describe("read_file tool with maxReadFileLine setting", () => {
mockedPathResolve.mockReturnValue(absoluteFilePath)
mockedIsBinaryFile.mockResolvedValue(false)
// Default mock for validateFileSizeForContext - no limit
vi.mocked(contextValidatorModule.validateFileSizeForContext).mockResolvedValue({
shouldLimit: false,
safeMaxLines: -1,
})
mockInputContent = fileContent
// Setup the extractTextFromFile mock implementation with the current mockInputContent
@ -519,4 +527,67 @@ describe("read_file tool XML output structure", () => {
)
})
})
describe("line range instructions", () => {
beforeEach(() => {
// Reset mocks
vi.clearAllMocks()
// Mock file system functions
vi.mocked(isBinaryFile).mockResolvedValue(false)
vi.mocked(countFileLines).mockResolvedValue(10000) // Large file
vi.mocked(readLines).mockResolvedValue("line content")
vi.mocked(extractTextFromFile).mockResolvedValue("file content")
// Mock addLineNumbers
vi.mocked(addLineNumbers).mockImplementation((content, start) => `${start || 1} | ${content}`)
})
it("should always include inline line_range instructions when shouldLimit is true", async () => {
// Mock a large file
vi.mocked(countFileLines).mockResolvedValue(10000)
// Mock contextValidator to return shouldLimit true
vi.mocked(contextValidatorModule.validateFileSizeForContext).mockResolvedValue({
shouldLimit: true,
safeMaxLines: 2000,
reason: "File exceeds available context space",
})
// Mock readLines to return truncated content
vi.mocked(readLines).mockResolvedValue("Line 1\nLine 2\n...truncated...")
const result = await executeReadFileTool(
{ args: `<file><path>large-file.ts</path></file>` },
{ totalLines: 10000, maxReadFileLine: -1 },
)
// Verify the result contains the inline instructions
expect(result).toContain("<notice>")
expect(result).toContain("File exceeds available context space")
expect(result).toContain("To read specific sections of this file, use the following format:")
expect(result).toContain("<line_range>start-end</line_range>")
expect(result).toContain("For example, to read lines 2001-3000:")
expect(result).toContain("<line_range>2001-3000</line_range>")
expect(result).toContain("large-file.ts")
})
it("should not show any special notice when file fits in context", async () => {
// Mock small file that fits in context
vi.mocked(countFileLines).mockResolvedValue(100)
vi.mocked(contextValidatorModule.validateFileSizeForContext).mockResolvedValue({
shouldLimit: false,
safeMaxLines: -1,
})
const result = await executeReadFileTool({ args: `<file><path>small-file.ts</path></file>` })
// Should have file content but no notice about limits
expect(result).toContain("<file>")
expect(result).toContain("<path>small-file.ts</path>")
expect(result).toContain("<content")
expect(result).not.toContain("Use line_range")
expect(result).not.toContain("File exceeds available context space")
})
})
})

View file

@ -0,0 +1,171 @@
import { promises as fs } from "fs"
import { Task } from "../task/Task"
import { readLines } from "../../integrations/misc/read-lines"
import { getModelMaxOutputTokens } from "../../shared/api"
/**
* More aggressive buffer percentage specifically for file reading validation.
* This is separate from the global TOKEN_BUFFER_PERCENTAGE to provide extra safety
* when reading files without affecting other context window calculations.
*/
const FILE_READ_BUFFER_PERCENTAGE = 0.25 // 25% buffer for file reads
export interface ContextValidationResult {
shouldLimit: boolean
safeMaxLines: number
reason?: string
}
/**
* Validates if a file can be safely read based on its size and current runtime context state.
* Reads lines incrementally and counts tokens as it goes, stopping when reaching the token limit.
* Returns a safe maxReadFileLine value to prevent context overflow.
*/
export async function validateFileSizeForContext(
filePath: string,
totalLines: number,
currentMaxReadFileLine: number,
cline: Task,
): Promise<ContextValidationResult> {
try {
// Get actual runtime state from the task
const modelInfo = cline.api.getModel().info
const { contextTokens: currentContextTokens } = cline.getTokenUsage()
const contextWindow = modelInfo.contextWindow
// Get the model-specific max output tokens using the same logic as sliding window
const modelId = cline.api.getModel().id
const apiProvider = cline.apiConfiguration.apiProvider
const settings = await cline.providerRef.deref()?.getState()
// Map apiProvider to the format expected by getModelMaxOutputTokens
let format: "anthropic" | "openai" | "gemini" | "openrouter" | undefined
if (
apiProvider === "anthropic" ||
apiProvider === "bedrock" ||
apiProvider === "vertex" ||
apiProvider === "claude-code"
) {
format = "anthropic"
} else if (apiProvider === "openrouter") {
format = "openrouter"
} else if (apiProvider === "openai" || apiProvider === "openai-native") {
format = "openai"
} else if (apiProvider === "gemini" || apiProvider === "gemini-cli") {
format = "gemini"
}
const maxResponseTokens = getModelMaxOutputTokens({ modelId, model: modelInfo, settings, format })
// Calculate how much context is already used
const currentlyUsed = currentContextTokens || 0
// Calculate remaining context space
const remainingContext = contextWindow - currentlyUsed
// Apply buffer to the remaining context, not the total context window
// This gives us a more accurate assessment of what's actually available
const usableRemainingContext = Math.floor(remainingContext * (1 - FILE_READ_BUFFER_PERCENTAGE))
// Use the same approach as sliding window: reserve the model's max tokens
// This ensures consistency across the codebase
const reservedForResponse = maxResponseTokens || 0
// Calculate available tokens for file content
const availableTokensForFile = usableRemainingContext - reservedForResponse
// Now read lines incrementally and count tokens until we reach the limit
const BATCH_SIZE = 100 // Read 100 lines at a time
let currentLine = 0
let totalTokensSoFar = 0
let safeMaxLines = 0
// Use 90% of available space to leave some margin
const targetTokenLimit = Math.floor(availableTokensForFile * 0.9)
while (currentLine < totalLines && totalTokensSoFar < targetTokenLimit) {
// Calculate the end line for this batch
const batchEndLine = Math.min(currentLine + BATCH_SIZE - 1, totalLines - 1)
try {
// Read the next batch of lines
const batchContent = await readLines(filePath, batchEndLine, currentLine)
// Count tokens for this batch
const batchTokens = await cline.api.countTokens([{ type: "text", text: batchContent }])
// Check if adding this batch would exceed our limit
if (totalTokensSoFar + batchTokens > targetTokenLimit) {
// This batch would exceed the limit
// Try to find a more precise cutoff within this batch
if (batchEndLine - currentLine > 10) {
// Read smaller chunks to find a more precise cutoff
const FINE_BATCH_SIZE = 10
let fineLine = currentLine
while (fineLine <= batchEndLine && totalTokensSoFar < targetTokenLimit) {
const fineEndLine = Math.min(fineLine + FINE_BATCH_SIZE - 1, batchEndLine)
const fineContent = await readLines(filePath, fineEndLine, fineLine)
const fineTokens = await cline.api.countTokens([{ type: "text", text: fineContent }])
if (totalTokensSoFar + fineTokens > targetTokenLimit) {
// Even this fine batch exceeds the limit
break
}
totalTokensSoFar += fineTokens
safeMaxLines = fineEndLine + 1 // Convert to 1-based line count
fineLine = fineEndLine + 1
}
}
// Stop processing more batches
break
}
// Add this batch's tokens to our total
totalTokensSoFar += batchTokens
safeMaxLines = batchEndLine + 1 // Convert to 1-based line count
currentLine = batchEndLine + 1
} catch (error) {
// If we encounter an error reading a batch, stop here
break
}
}
// Ensure we provide at least a minimum useful amount
const minUsefulLines = 50
const finalSafeMaxLines = Math.max(minUsefulLines, safeMaxLines)
// If we read the entire file without exceeding the limit, no limitation needed
if (safeMaxLines >= totalLines) {
return { shouldLimit: false, safeMaxLines: currentMaxReadFileLine }
}
// If we couldn't read even the minimum useful lines
if (safeMaxLines < minUsefulLines) {
return {
shouldLimit: true,
safeMaxLines: finalSafeMaxLines,
reason: `Very limited context space. Could only safely read ${safeMaxLines} lines before exceeding token limit. Context: ${currentlyUsed}/${contextWindow} tokens used (${Math.round((currentlyUsed / contextWindow) * 100)}%). Limited to ${finalSafeMaxLines} lines. Consider using search_files or line_range for specific sections.`,
}
}
return {
shouldLimit: true,
safeMaxLines: finalSafeMaxLines,
reason: `File exceeds available context space. Safely read ${finalSafeMaxLines} lines (${totalTokensSoFar} tokens) out of ${totalLines} total lines. Context usage: ${currentlyUsed}/${contextWindow} tokens (${Math.round((currentlyUsed / contextWindow) * 100)}%). Use line_range to read specific sections.`,
}
} catch (error) {
// If we can't get runtime state, fall back to conservative estimation
console.warn(`[validateFileSizeForContext] Error accessing runtime state: ${error}`)
if (totalLines > 10000) {
return {
shouldLimit: true,
safeMaxLines: 1000,
reason: "Large file detected (>10,000 lines). Limited to 1000 lines to prevent context overflow (runtime state unavailable).",
}
}
return { shouldLimit: false, safeMaxLines: currentMaxReadFileLine }
}
}

View file

@ -2,6 +2,7 @@ import path from "path"
import { isBinaryFile } from "isbinaryfile"
import { Task } from "../task/Task"
import { validateFileSizeForContext } from "./contextValidator"
import { ClineSayTool } from "../../shared/ExtensionMessage"
import { formatResponse } from "../prompts/responses"
import { t } from "../../i18n"
@ -435,6 +436,21 @@ export async function readFileTool(
try {
const [totalLines, isBinary] = await Promise.all([countFileLines(fullPath), isBinaryFile(fullPath)])
// Preemptive file size validation to prevent context overflow
const validation = await validateFileSizeForContext(fullPath, totalLines, maxReadFileLine, cline)
let effectiveMaxReadFileLine = maxReadFileLine
let validationNotice = ""
if (validation.shouldLimit && maxReadFileLine === -1) {
// Only apply limitation if maxReadFileLine is -1 (unlimited)
// If user has already set a limit, respect their choice
effectiveMaxReadFileLine = validation.safeMaxLines
validationNotice = validation.reason || ""
console.log(
`[read_file] Applied preemptive size limit to ${relPath}: ${validation.safeMaxLines} lines`,
)
}
// Handle binary files (but allow specific file types that extractTextFromFile can handle)
if (isBinary) {
const fileExtension = path.extname(relPath).toLowerCase()
@ -468,11 +484,11 @@ export async function readFileTool(
}
// Handle definitions-only mode
if (maxReadFileLine === 0) {
if (effectiveMaxReadFileLine === 0) {
try {
const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController)
if (defResult) {
let xmlInfo = `<notice>Showing only ${maxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines</notice>\n`
let xmlInfo = `<notice>Showing only ${effectiveMaxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines</notice>\n`
updateFileResult(relPath, {
xmlContent: `<file><path>${relPath}</path>\n<list_code_definition_names>${defResult}</list_code_definition_names>\n${xmlInfo}</file>`,
})
@ -489,10 +505,10 @@ export async function readFileTool(
continue
}
// Handle files exceeding line threshold
if (maxReadFileLine > 0 && totalLines > maxReadFileLine) {
const content = addLineNumbers(await readLines(fullPath, maxReadFileLine - 1, 0))
const lineRangeAttr = ` lines="1-${maxReadFileLine}"`
// Handle files exceeding line threshold (including preemptive limits)
if (effectiveMaxReadFileLine > 0 && totalLines > effectiveMaxReadFileLine) {
const content = addLineNumbers(await readLines(fullPath, effectiveMaxReadFileLine - 1, 0))
const lineRangeAttr = ` lines="1-${effectiveMaxReadFileLine}"`
let xmlInfo = `<content${lineRangeAttr}>\n${content}</content>\n`
try {
@ -500,7 +516,15 @@ export async function readFileTool(
if (defResult) {
xmlInfo += `<list_code_definition_names>${defResult}</list_code_definition_names>\n`
}
xmlInfo += `<notice>Showing only ${maxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines</notice>\n`
// Add appropriate notice based on whether this was a preemptive limit or user setting
if (validationNotice) {
// When shouldLimit is true, always provide inline instructions
xmlInfo += `<notice>${validationNotice}\n\nTo read specific sections of this file, use the following format:\n<read_file>\n<args>\n <file>\n <path>${relPath}</path>\n <line_range>start-end</line_range>\n </file>\n</args>\n</read_file>\n\nFor example, to read lines 2001-3000:\n<read_file>\n<args>\n <file>\n <path>${relPath}</path>\n <line_range>2001-3000</line_range>\n </file>\n</args>\n</read_file></notice>\n`
} else {
xmlInfo += `<notice>Showing only ${effectiveMaxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines</notice>\n`
}
updateFileResult(relPath, {
xmlContent: `<file><path>${relPath}</path>\n${xmlInfo}</file>`,
})