From 5c482531480e1607808fee1523aaefc5aac6e55c Mon Sep 17 00:00:00 2001 From: Will Li Date: Mon, 28 Jul 2025 15:40:28 -0700 Subject: [PATCH] working --- .../tools/__tests__/contextValidator.test.ts | 380 ++++++++++++++++++ src/core/tools/__tests__/readFileTool.spec.ts | 73 +++- src/core/tools/contextValidator.ts | 171 ++++++++ src/core/tools/readFileTool.ts | 38 +- 4 files changed, 654 insertions(+), 8 deletions(-) create mode 100644 src/core/tools/__tests__/contextValidator.test.ts create mode 100644 src/core/tools/contextValidator.ts 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}`, })