diff --git a/src/core/tools/__tests__/contextValidator.test.ts b/src/core/tools/__tests__/contextValidator.test.ts
new file mode 100644
index 0000000000..e8497f6bcf
--- /dev/null
+++ b/src/core/tools/__tests__/contextValidator.test.ts
@@ -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")
+ })
+ })
+})
diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts
index 44be1d3b92..c0f50e59d5 100644
--- a/src/core/tools/__tests__/readFileTool.spec.ts
+++ b/src/core/tools/__tests__/readFileTool.spec.ts
@@ -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: `large-file.ts` },
+ { totalLines: 10000, maxReadFileLine: -1 },
+ )
+
+ // Verify the result contains the inline instructions
+ expect(result).toContain("")
+ 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("start-end")
+ expect(result).toContain("For example, to read lines 2001-3000:")
+ expect(result).toContain("2001-3000")
+ 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: `small-file.ts` })
+
+ // Should have file content but no notice about limits
+ expect(result).toContain("")
+ expect(result).toContain("small-file.ts")
+ expect(result).toContain(" {
+ 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 }
+ }
+}
diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts
index 6de8dd5642..0959ec2b61 100644
--- a/src/core/tools/readFileTool.ts
+++ b/src/core/tools/readFileTool.ts
@@ -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 = `Showing only ${maxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines\n`
+ let xmlInfo = `Showing only ${effectiveMaxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines\n`
updateFileResult(relPath, {
xmlContent: `${relPath}\n${defResult}\n${xmlInfo}`,
})
@@ -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 = `\n${content}\n`
try {
@@ -500,7 +516,15 @@ export async function readFileTool(
if (defResult) {
xmlInfo += `${defResult}\n`
}
- xmlInfo += `Showing only ${maxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines\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 += `${validationNotice}\n\nTo read specific sections of this file, use the following format:\n\n\n \n ${relPath}\n start-end\n \n\n\n\nFor example, to read lines 2001-3000:\n\n\n \n ${relPath}\n 2001-3000\n \n\n\n`
+ } else {
+ xmlInfo += `Showing only ${effectiveMaxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines\n`
+ }
+
updateFileResult(relPath, {
xmlContent: `${relPath}\n${xmlInfo}`,
})