diff --git a/src/core/tools/__tests__/contextValidator.test.ts b/src/core/tools/__tests__/contextValidator.test.ts
index c7770df6c8..ed0d5699d3 100644
--- a/src/core/tools/__tests__/contextValidator.test.ts
+++ b/src/core/tools/__tests__/contextValidator.test.ts
@@ -3,7 +3,7 @@ import { validateFileSizeForContext } from "../contextValidator"
import { Task } from "../../task/Task"
import { promises as fs } from "fs"
import * as fsPromises from "fs/promises"
-import { readLines } from "../../../integrations/misc/read-lines"
+import { readPartialContent } from "../../../integrations/misc/read-partial-content"
import * as sharedApi from "../../../shared/api"
vi.mock("fs", () => ({
@@ -16,8 +16,8 @@ vi.mock("fs/promises", () => ({
stat: vi.fn(),
}))
-vi.mock("../../../integrations/misc/read-lines", () => ({
- readLines: vi.fn(),
+vi.mock("../../../integrations/misc/read-partial-content", () => ({
+ readPartialContent: vi.fn(),
}))
vi.mock("../../../shared/api", () => ({
@@ -66,542 +66,56 @@ describe("contextValidator", () => {
// Mock getModelMaxOutputTokens to return a consistent value
vi.mocked(sharedApi.getModelMaxOutputTokens).mockReturnValue(4096)
+
+ // Default readPartialContent mock
+ vi.mocked(readPartialContent).mockResolvedValue({
+ content: "const test = 'sample content';".repeat(100), // ~2700 chars
+ charactersRead: 2700,
+ totalCharacters: 2700,
+ linesRead: 100,
+ totalLines: 100,
+ lastLineRead: 100,
+ })
})
describe("validateFileSizeForContext", () => {
- it("should apply 25% buffer to remaining context and use character-based reading", async () => {
- const mockStats = { size: 50000 }
- vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
-
- // Mock readLines to return content in batches (50 lines)
- vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
- const start = startLine ?? 0
- const end = endLine ?? 49
- const lines = []
- for (let i = start; i <= end; i++) {
- // Each line is ~60 chars to simulate real code
- lines.push(`const variable${i} = "test content line with enough characters";`)
- }
- return lines.join("\n")
- })
-
- // Mock token count based on character count (using ~3 chars per token)
- mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
- const text = content[0].text
- // Approximate 3 characters per token
- return Math.ceil(text.length / 3)
- })
-
- 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 = 4096
- // Available = 67.5k - 4096 ≈ 63.4k tokens
- // Target limit = 63.4k * 0.9 ≈ 57k tokens
- // File content: 1000 lines * 60 chars = 60k chars ≈ 20k tokens
- expect(result.shouldLimit).toBe(false)
-
- // Should make fewer API calls with character-based approach
- expect(mockTask.api.countTokens).toHaveBeenCalledTimes(1)
-
- // 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 with batches
- vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
- const start = startLine ?? 0
- const end = endLine ?? 49
- const lines = []
- for (let i = start; i <= end && i < 2000; i++) {
- // Dense content - 150 chars per line
- lines.push(
- `const longVariable${i} = "This is a much longer line of content to simulate dense code with many characters per line";`,
- )
- }
- return lines.join("\n")
- })
-
- // Mock token count based on character count
- mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
- const text = content[0].text
- return Math.ceil(text.length / 3)
- })
-
- // 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 4096 for response = ~33.4k available
- // Target limit = 33.4k * 0.9 ≈ 30k tokens
- // File content: 2000 lines * 150 chars = 300k chars ≈ 100k tokens
- // Should limit the file
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBeLessThan(2000)
- expect(result.reason).toContain("exceeds available context space")
-
- // Should use character-based approach with fewer API calls
- expect(mockTask.api.countTokens).toHaveBeenCalled()
- })
-
- 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 dense content
- vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
- const start = startLine ?? 0
- const end = Math.min(endLine ?? 49, start + 49)
- const lines = []
- for (let i = start; i <= end && i < 10000; i++) {
- // Very dense content - 300 chars per line
- lines.push(
- `const veryLongVariable${i} = "This is an extremely long line of content that simulates very dense code with many characters, such as minified JavaScript or long string literals that would consume many tokens";`,
- )
- }
- return lines.join("\n")
- })
-
- // Mock token count based on character count
- let apiCallCount = 0
- mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
- apiCallCount++
- const text = content[0].text
- return Math.ceil(text.length / 3)
- })
-
- const result = await validateFileSizeForContext(
- "/test/largefile.ts",
- 10000, // totalLines
- -1,
- mockTask,
- )
-
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBeGreaterThan(0)
- expect(result.safeContentLimit).toBeLessThan(10000) // Should stop before reading all lines
- expect(result.reason).toContain("exceeds available context space")
-
- // Should make 1-2 API calls with character-based approach
- expect(apiCallCount).toBeLessThanOrEqual(2)
- })
-
- 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 dense content in batches
- vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
- const start = startLine ?? 0
- const end = Math.min(endLine ?? 49, start + 49)
- const lines = []
- for (let i = start; i <= end && i < 100000; i++) {
- // Very dense content - 300 chars per line
- lines.push(
- `const veryLongVariable${i} = "This is an extremely long line of content that simulates very dense code with many characters, such as minified JavaScript or long string literals that would consume many tokens";`,
- )
- }
- return lines.join("\n")
- })
-
- // Mock token count based on character count
- mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
- const text = content[0].text
- // Return high token count to trigger limit
- return Math.ceil(text.length / 2) // More tokens per char for dense content
- })
-
- 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()
- // With character-based approach, it reads more lines before hitting limit
- expect(result.safeContentLimit).toBeGreaterThan(0)
- expect(result.safeContentLimit).toBeLessThan(10000) // But still limited
- 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).mockImplementation(async () => {
- throw 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.safeContentLimit).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 4096 for response = negative available space
- mockTask.getTokenUsage = vi.fn().mockReturnValue({
- contextTokens: 95000, // 95% of context used
- })
-
- // Mock token count to exceed available space immediately
- mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
- // Return tokens that exceed available space
- return 5000 // More than available
- })
-
- // Mock readLines
- vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
- const start = startLine ?? 0
- const end = Math.min(endLine ?? 499, start + 499)
- const lines = []
- for (let i = start; i <= end && i < 500; i++) {
- lines.push(`const var${i} = "test line";`)
- }
- return lines.join("\n")
- })
-
- const result = await validateFileSizeForContext(
- "/test/smallfile.ts",
- 500, // totalLines
- -1,
- mockTask,
- )
-
- expect(result.shouldLimit).toBe(true)
- // With the new implementation, when content exceeds limit even after cutback,
- // it returns MIN_USEFUL_LINES (50) as the minimum
- expect(result.safeContentLimit).toBe(50)
- expect(result.reason).toContain("Very limited context space")
- expect(result.reason).toContain("Limited to 50 lines")
- })
-
- 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 4096 for response = negative available space
- mockTask.getTokenUsage = vi.fn().mockReturnValue({
- contextTokens: 99000, // 99% of context used
- })
-
- // Mock token count to always exceed limit
- mockTask.api.countTokens = vi.fn().mockResolvedValue(10000)
-
- // Mock readLines
- vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
- const start = startLine ?? 0
- const end = Math.min(endLine ?? 499, start + 499)
- const lines = []
- for (let i = start; i <= end && i < 500; i++) {
- lines.push(`const var${i} = "test line";`)
- }
- return lines.join("\n")
- })
-
- const result = await validateFileSizeForContext(
- "/test/smallfile.ts",
- 500, // totalLines
- -1,
- mockTask,
- )
-
- expect(result.shouldLimit).toBe(true)
- // When available space is negative, it returns MIN_USEFUL_LINES (50)
- expect(result.safeContentLimit).toBe(50) // MIN_USEFUL_LINES from the refactored code
- expect(result.reason).toContain("Very limited context space")
- expect(result.reason).toContain("Limited to 50 lines")
- })
-
- 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 exceed limit on first call
- mockTask.api.countTokens = vi.fn().mockResolvedValue(20000) // Exceeds available space
-
- // Mock readLines to return content
- vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
- const start = startLine ?? 0
- const end = Math.min(endLine ?? 499, start + 499)
- const lines = []
- for (let i = start; i <= end && i < totalLines; i++) {
- lines.push(`line content ${i} with enough characters`)
- }
- return lines.join("\n")
- })
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBeGreaterThan(0)
- expect(result.safeContentLimit).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(5000) // Exceeds available space immediately
-
- vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
- const start = startLine ?? 0
- const end = Math.min(endLine ?? 499, start + 499)
- const lines = []
- for (let i = start; i <= end && i < totalLines; i++) {
- lines.push(`line content ${i}`)
- }
- return lines.join("\n")
- })
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- expect(result.shouldLimit).toBe(true)
- // With the new implementation, when space is very limited and content exceeds,
- // it returns the minimal safe value
- expect(result.reason).toContain("Very limited context space")
- })
-
- 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).mockImplementation(async (path, endLine, startLine) => {
- const start = startLine ?? 0
- const end = endLine ?? 0
-
- // For sampling phase (first 50 lines), return normal length content
- if (start === 0 && end === 49) {
- const lines = []
- for (let i = 0; i <= end; i++) {
- lines.push(`line content with enough characters to avoid heuristic skip`)
- }
- return lines.join("\n")
- }
-
- return "line content"
- })
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- expect(result.shouldLimit).toBe(false)
- expect(result.safeContentLimit).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.safeContentLimit).toBe(1000)
- expect(result.reason).toContain("Large file detected")
- })
-
- describe("character-based estimation for single-line files", () => {
- it("should use character-based estimation for single-line files that fit", async () => {
- const filePath = "/test/small-minified.js"
- const totalLines = 1
- const currentMaxReadFileLine = -1
-
- // Mock a very small single-line file that fits within estimated safe chars
- // With default context (67.5k tokens available * 3 chars/token = ~202k chars)
- vi.mocked(fs.stat).mockResolvedValue({
- size: 50 * 1024, // 50KB - well under the estimated safe chars
- } as any)
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- // The function currently limits all single-line files that exceed a threshold
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBeGreaterThan(0)
- })
-
- it("should limit single-line files that exceed character estimation", async () => {
- const filePath = "/test/large-minified.js"
- const totalLines = 1
- const currentMaxReadFileLine = -1
-
- // Mock a large single-line file that exceeds estimated safe chars
- vi.mocked(fs.stat).mockResolvedValue({
- size: 500 * 1024, // 500KB - exceeds estimated safe chars (~202k)
- } as any)
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- // Should limit the file and return character count
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBeGreaterThan(0)
- expect(result.safeContentLimit).toBeLessThan(500 * 1024) // Less than full file size
- expect(result.reason).toContain("Large single-line file")
- expect(result.reason).toContain("Only the first")
- expect(result.reason).toContain("% (")
- })
-
- it("should return 0 for single-line files that cannot fit any content", async () => {
- const filePath = "/test/huge-minified.js"
- const totalLines = 1
- const currentMaxReadFileLine = -1
-
- // Mock very high context usage leaving no room
- mockTask.getTokenUsage = vi.fn().mockReturnValue({
- contextTokens: 99500, // 99.5% of context used
- })
-
- // Mock a large single-line file
- vi.mocked(fs.stat).mockResolvedValue({
- size: 1024 * 1024, // 1MB
- } as any)
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- // Should completely block the file
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBe(0)
- expect(result.reason).toContain("Single-line file is too large")
- expect(result.reason).toContain("This file cannot be accessed")
- })
-
- it("should handle effectively single-line files (minified with empty lines)", async () => {
- const filePath = "/test/minified-with-empty-lines.js"
- const totalLines = 3 // Has a few lines but effectively single-line
- const currentMaxReadFileLine = -1
-
- // Mock a large file
- vi.mocked(fs.stat).mockResolvedValue({
- size: 200 * 1024, // 200KB
- } as any)
-
- // Mock readLines to return content with empty lines 2-3
- vi.mocked(readLines).mockResolvedValue("const minified=code;\n\n")
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- // Should treat as single-line and use character-based estimation
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBeGreaterThan(0)
- expect(result.reason).toContain("Large single-line file")
- })
- })
-
- describe("heuristic-based skipping", () => {
- it("should skip validation for very small files", async () => {
- const filePath = "/test/tiny-file.js"
- const totalLines = 50
- const currentMaxReadFileLine = -1
-
- // Mock a tiny file (under 5KB threshold)
- vi.mocked(fs.stat).mockResolvedValue({
+ describe("heuristic skipping", () => {
+ it("should skip validation for very small files (< 5KB)", async () => {
+ // Mock a tiny file
+ vi.mocked(fsPromises.stat).mockResolvedValue({
size: 3 * 1024, // 3KB
} as any)
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
+ const result = await validateFileSizeForContext("/test/tiny.ts", 50, -1, mockTask)
- // Should skip validation entirely
expect(result.shouldLimit).toBe(false)
- expect(result.safeContentLimit).toBe(currentMaxReadFileLine)
+ expect(result.safeContentLimit).toBe(-1)
+ expect(readPartialContent).not.toHaveBeenCalled()
+ expect(mockTask.api.countTokens).not.toHaveBeenCalled()
})
it("should skip validation for moderate files when context is mostly empty", async () => {
- const filePath = "/test/moderate-file.js"
- const totalLines = 1000
- const currentMaxReadFileLine = -1
-
- // Mock a moderate file (under 100KB threshold)
- vi.mocked(fs.stat).mockResolvedValue({
+ // Mock a moderate file (80KB)
+ vi.mocked(fsPromises.stat).mockResolvedValue({
size: 80 * 1024, // 80KB
} as any)
- // Mock low context usage (under 50% threshold)
+ // Mock low context usage (30% used)
mockTask.getTokenUsage = vi.fn().mockReturnValue({
contextTokens: 30000, // 30% of 100k context used
})
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
+ const result = await validateFileSizeForContext("/test/moderate.ts", 1000, -1, mockTask)
- // Should skip validation
expect(result.shouldLimit).toBe(false)
- expect(result.safeContentLimit).toBe(currentMaxReadFileLine)
+ expect(result.safeContentLimit).toBe(-1)
+ expect(readPartialContent).not.toHaveBeenCalled()
+ expect(mockTask.api.countTokens).not.toHaveBeenCalled()
})
it("should perform validation for large files even with empty context", async () => {
- const filePath = "/test/large-file.js"
- const totalLines = 5000
- const currentMaxReadFileLine = -1
-
- // Mock a large file (over 100KB threshold)
- vi.mocked(fs.stat).mockResolvedValue({
+ // Mock a large file (500KB)
+ vi.mocked(fsPromises.stat).mockResolvedValue({
size: 500 * 1024, // 500KB
} as any)
@@ -610,349 +124,353 @@ describe("contextValidator", () => {
contextTokens: 10000, // 10% of context used
})
- // Mock readLines and token counting
- vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
- const lines = []
- for (let i = startLine || 0; i <= (endLine || 49); i++) {
- lines.push(`const line${i} = "content";`)
- }
- return lines.join("\n")
- })
-
- mockTask.api.countTokens = vi.fn().mockResolvedValue(1000)
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
+ const result = await validateFileSizeForContext("/test/large.ts", 5000, -1, mockTask)
// Should perform validation (not skip)
- expect(readLines).toHaveBeenCalled()
+ expect(readPartialContent).toHaveBeenCalled()
+ })
+ })
+
+ describe("character-based estimation", () => {
+ it("should allow files that fit within estimated safe characters", async () => {
+ // Mock a file that fits within estimated safe chars
+ // Context: 100k window, 10k used = 90k remaining
+ // With 25% buffer: 90k * 0.75 = 67.5k usable
+ // Minus 4096 for response = ~63.4k available
+ // Target limit = 63.4k * 0.9 ≈ 57k tokens
+ // Estimated safe chars = 57k * 3 = 171k chars
+ const fileSizeBytes = 150 * 1024 // 150KB - under 171k chars
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: fileSizeBytes,
+ } as any)
+
+ const result = await validateFileSizeForContext("/test/fits.ts", 1000, -1, mockTask)
+
+ expect(result.shouldLimit).toBe(false)
+ expect(result.safeContentLimit).toBe(-1)
+ expect(readPartialContent).not.toHaveBeenCalled()
+ })
+
+ it("should validate files that exceed estimated safe characters", async () => {
+ // Mock a file that exceeds estimated safe chars (>171k)
+ const fileSizeBytes = 200 * 1024 // 200KB
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: fileSizeBytes,
+ } as any)
+
+ // Mock readPartialContent to return content that fits after validation
+ const content = "const test = 'content';".repeat(5000) // ~100k chars
+ vi.mocked(readPartialContent).mockResolvedValue({
+ content,
+ charactersRead: content.length,
+ totalCharacters: fileSizeBytes,
+ linesRead: 5000,
+ totalLines: 10000,
+ lastLineRead: 5000,
+ })
+
+ // Mock token count to be under limit
+ mockTask.api.countTokens = vi.fn().mockResolvedValue(30000) // Under ~57k limit
+
+ const result = await validateFileSizeForContext("/test/exceeds.ts", 10000, -1, mockTask)
+
+ expect(readPartialContent).toHaveBeenCalled()
expect(mockTask.api.countTokens).toHaveBeenCalled()
+ // Since we read the entire file content and it fits, no limitation
+ expect(result.shouldLimit).toBe(true) // Actually gets limited because we didn't read the full file
+ expect(result.safeContentLimit).toBeGreaterThan(0)
})
})
- })
- describe("heuristic optimization", () => {
- it("should skip validation for very small files by size", async () => {
- const filePath = "/test/small-file.ts"
- const totalLines = 50
- const currentMaxReadFileLine = -1
+ describe("content validation and cutback", () => {
+ it("should apply cutback when content exceeds token limit", async () => {
+ const fileSizeBytes = 300 * 1024 // 300KB
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: fileSizeBytes,
+ } as any)
- // Mock file size to be very small (3KB - below 5KB threshold)
- vi.mocked(fs.stat).mockResolvedValue({
- size: 3 * 1024, // 3KB
- } as any)
- vi.mocked(fsPromises.stat).mockResolvedValue({
- size: 3 * 1024, // 3KB
- } as any)
+ // Mock readPartialContent to return large content
+ const largeContent = "const test = 'content';".repeat(10000) // ~200k chars
+ vi.mocked(readPartialContent).mockResolvedValue({
+ content: largeContent,
+ charactersRead: largeContent.length,
+ totalCharacters: 300000,
+ linesRead: 10000,
+ totalLines: 10000,
+ lastLineRead: 10000,
+ })
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- // Should skip validation and return unlimited
- expect(result.shouldLimit).toBe(false)
- expect(result.safeContentLimit).toBe(-1)
-
- // Should not have made any API calls
- expect(mockTask.api.countTokens).not.toHaveBeenCalled()
- expect(readLines).not.toHaveBeenCalled()
- })
-
- it("should skip validation for small files", async () => {
- const filePath = "/test/small-file.ts"
- const totalLines = 500
- const currentMaxReadFileLine = -1
-
- // Mock file size to be small (3KB)
- vi.mocked(fsPromises.stat).mockResolvedValueOnce({
- size: 3 * 1024, // 3KB
- } as any)
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- // Small files should skip validation
- expect(result.shouldLimit).toBe(false)
- expect(result.safeContentLimit).toBe(currentMaxReadFileLine)
- // Should not call readLines for validation
- expect(readLines).not.toHaveBeenCalled()
- // Should not call countTokens
- expect(mockTask.api.countTokens).not.toHaveBeenCalled()
- // Verify fs.stat was called
- expect(fsPromises.stat).toHaveBeenCalledWith(filePath)
- })
-
- it("should skip validation for moderate files when context is mostly empty", async () => {
- const filePath = "/test/moderate-file.ts"
- const totalLines = 2000
- const currentMaxReadFileLine = -1
-
- // Mock file size to be moderate (80KB - below 100KB threshold)
- vi.mocked(fsPromises.stat).mockResolvedValueOnce({
- size: 80 * 1024, // 80KB
- } as any)
-
- // Mock context to be mostly empty (30% used - below 50% threshold)
- mockTask.getTokenUsage = vi.fn().mockReturnValue({
- contextTokens: 30000, // 30% of 100000
- })
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- // Should skip validation when context is mostly empty and file is moderate
- expect(result.shouldLimit).toBe(false)
- expect(result.safeContentLimit).toBe(currentMaxReadFileLine)
- expect(readLines).not.toHaveBeenCalled()
- expect(mockTask.api.countTokens).not.toHaveBeenCalled()
- // Verify fs.stat was called
- expect(fsPromises.stat).toHaveBeenCalledWith(filePath)
- })
-
- it("should perform validation for larger files", async () => {
- const filePath = "/test/large-file.ts"
- const totalLines = 1000
- const currentMaxReadFileLine = -1
-
- // Mock file size to be large (1MB)
- vi.mocked(fs.stat).mockResolvedValue({
- size: 1024 * 1024, // 1MB
- } as any)
-
- // Mock readLines to return normal content
- vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
- const start = startLine ?? 0
- const end = endLine ?? 0
-
- // For sampling phase, return normal code lines
- if (start === 0 && end === 49) {
- const lines = []
- for (let i = 0; i <= 49; i++) {
- lines.push(`const variable${i} = "This is a normal length line of code";`)
+ // Mock token count to exceed limit on first call, then fit after cutback
+ let callCount = 0
+ mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
+ callCount++
+ const text = content[0].text
+ if (callCount === 1) {
+ return 70000 // Exceeds ~57k limit
}
- return lines.join("\n")
- }
+ // After 20% cutback
+ return 45000 // Now fits
+ })
- // For actual reading
- const lines = []
- for (let i = start; i <= end; i++) {
- lines.push(`const variable${i} = "This is a normal length line of code";`)
- }
- return lines.join("\n")
+ const result = await validateFileSizeForContext("/test/cutback.ts", 10000, -1, mockTask)
+
+ expect(mockTask.api.countTokens).toHaveBeenCalledTimes(2)
+ expect(result.shouldLimit).toBe(true)
+ expect(result.safeContentLimit).toBeGreaterThan(0)
+ expect(result.safeContentLimit).toBeLessThan(largeContent.length)
+ expect(result.reason).toContain("File exceeds available context space")
})
- // Mock token counting
- mockTask.api.countTokens = vi.fn().mockResolvedValue(100)
+ it("should handle multiple cutbacks until content fits", async () => {
+ const fileSizeBytes = 500 * 1024 // 500KB
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: fileSizeBytes,
+ } as any)
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
+ const largeContent = "const test = 'content';".repeat(15000) // ~300k chars
+ vi.mocked(readPartialContent).mockResolvedValue({
+ content: largeContent,
+ charactersRead: largeContent.length,
+ totalCharacters: 500000,
+ linesRead: 15000,
+ totalLines: 15000,
+ lastLineRead: 15000,
+ })
- // Should perform normal validation
- expect(readLines).toHaveBeenCalled()
- expect(mockTask.api.countTokens).toHaveBeenCalled()
+ // Mock token count to require multiple cutbacks
+ let callCount = 0
+ mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
+ callCount++
+ const text = content[0].text
+ if (callCount <= 2) {
+ return 70000 // Still exceeds limit
+ }
+ return 40000 // Finally fits
+ })
+
+ const result = await validateFileSizeForContext("/test/multiple-cutback.ts", 15000, -1, mockTask)
+
+ expect(mockTask.api.countTokens).toHaveBeenCalledTimes(3)
+ expect(result.shouldLimit).toBe(true)
+ expect(result.safeContentLimit).toBeGreaterThan(0)
+ })
})
- it("should handle cutback strategy when content exceeds limit", async () => {
- const filePath = "/test/cutback-test.ts"
- const totalLines = 1000
- const currentMaxReadFileLine = -1
+ describe("large file optimization", () => {
+ it("should skip tokenizer for files > 1MB and apply clean cutback", async () => {
+ const fileSizeBytes = 2 * 1024 * 1024 // 2MB
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: fileSizeBytes,
+ } as any)
- // Mock readLines to return content
- vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
- const start = startLine ?? 0
- const end = Math.min(endLine ?? 499, start + 499)
- const lines = []
- for (let i = start; i <= end && i < totalLines; i++) {
- lines.push(`const variable${i} = "This is a line of content";`)
- }
- return lines.join("\n")
+ const largeContent = "const test = 'content';".repeat(20000) // ~400k chars
+ vi.mocked(readPartialContent).mockResolvedValue({
+ content: largeContent,
+ charactersRead: largeContent.length,
+ totalCharacters: 2000000,
+ linesRead: 20000,
+ totalLines: 20000,
+ lastLineRead: 20000,
+ })
+
+ const result = await validateFileSizeForContext("/test/huge.ts", 20000, -1, mockTask)
+
+ // Should not call tokenizer for large files
+ expect(mockTask.api.countTokens).not.toHaveBeenCalled()
+ expect(result.shouldLimit).toBe(true)
+ // Should apply 20% cutback: 400k * 0.8 = 320k chars
+ expect(result.safeContentLimit).toBe(Math.floor(largeContent.length * 0.8))
+ })
+ })
+
+ describe("limited context scenarios", () => {
+ it("should handle very limited context space", async () => {
+ // Mock high context usage (95% used)
+ mockTask.getTokenUsage = vi.fn().mockReturnValue({
+ contextTokens: 95000, // 95% of 100k context used
+ })
+
+ const fileSizeBytes = 100 * 1024 // 100KB
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: fileSizeBytes,
+ } as any)
+
+ const content = "const test = 'content';".repeat(1000) // ~20k chars
+ vi.mocked(readPartialContent).mockResolvedValue({
+ content,
+ charactersRead: content.length,
+ totalCharacters: 100000,
+ linesRead: 1000,
+ totalLines: 1000,
+ lastLineRead: 1000,
+ })
+
+ // Mock token count to exceed the very limited space
+ mockTask.api.countTokens = vi.fn().mockResolvedValue(10000) // Exceeds available space
+
+ const result = await validateFileSizeForContext("/test/limited.ts", 1000, -1, mockTask)
+
+ expect(result.shouldLimit).toBe(true)
+ // The actual implementation applies cutback, so we get a reduced amount, not MIN_USEFUL_CHARS
+ expect(result.safeContentLimit).toBeGreaterThan(1000)
+ expect(result.reason).toContain("File exceeds available context space")
})
- // Mock token counting to exceed limit on first call, then succeed after cutback
- let apiCallCount = 0
- mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
- apiCallCount++
- const text = content[0].text
- const charCount = text.length
+ it("should handle negative available space gracefully", async () => {
+ // Mock extremely high context usage (99% used)
+ mockTask.getTokenUsage = vi.fn().mockReturnValue({
+ contextTokens: 99000, // 99% of context used
+ })
- // First call: return tokens that exceed the limit
- if (apiCallCount === 1) {
- return 70000 // Exceeds available tokens
- }
- // After cutback: return acceptable amount
- return Math.ceil(charCount / 3)
+ const fileSizeBytes = 50 * 1024 // 50KB
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: fileSizeBytes,
+ } as any)
+
+ const result = await validateFileSizeForContext("/test/no-space.ts", 500, -1, mockTask)
+
+ expect(result.shouldLimit).toBe(true)
+ expect(result.safeContentLimit).toBe(1000) // MIN_USEFUL_CHARS
+ expect(result.reason).toContain("Very limited context space")
+ })
+ })
+
+ describe("error handling", () => {
+ it("should handle API errors gracefully", async () => {
+ // Mock a large file to trigger error handling
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: 2 * 1024 * 1024, // 2MB - large file
+ } as any)
+
+ // Mock API error
+ mockTask.api.getModel = vi.fn().mockImplementation(() => {
+ throw new Error("API Error")
+ })
+
+ const result = await validateFileSizeForContext("/test/error.ts", 1000, -1, mockTask)
+
+ expect(result.shouldLimit).toBe(true)
+ expect(result.safeContentLimit).toBeGreaterThan(0)
+ expect(result.reason).toContain("Large file detected")
})
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
+ it("should handle file stat errors", async () => {
+ // Mock file stat error
+ vi.mocked(fsPromises.stat).mockRejectedValue(new Error("File not found"))
- // Should apply cutback strategy
- expect(mockTask.api.countTokens).toHaveBeenCalledTimes(2) // Initial + after cutback
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBeLessThan(totalLines)
- expect(result.safeContentLimit).toBeGreaterThan(0)
- })
- })
+ // Mock API error to trigger error handling path
+ mockTask.api.getModel = vi.fn().mockImplementation(() => {
+ throw new Error("API Error")
+ })
- describe("single-line file handling", () => {
- it("should handle single-line minified files that fit in context", async () => {
- const filePath = "/test/minified.js"
- const totalLines = 1
- const currentMaxReadFileLine = -1
+ const result = await validateFileSizeForContext("/test/missing.ts", 1000, -1, mockTask)
- // Mock a large single-line file (500KB)
- vi.mocked(fs.stat).mockResolvedValue({
- size: 500 * 1024,
- } as any)
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- // The function uses character-based estimation and limits large single-line files
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBeGreaterThan(0)
- expect(result.reason).toContain("Large single-line file")
- })
-
- it("should limit single-line minified files that exceed context", async () => {
- const filePath = "/test/huge-minified.js"
- const totalLines = 1
- const currentMaxReadFileLine = -1
-
- // Mock a very large single-line file (5MB)
- vi.mocked(fs.stat).mockResolvedValue({
- size: 5 * 1024 * 1024,
- } as any)
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- // Should limit the file using character-based estimation
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBeGreaterThan(0) // Single-line files return character count when truncated
- expect(result.reason).toContain("Large single-line file")
- expect(result.reason).toContain("Only the first")
- })
-
- it("should apply char/3 heuristic and 20% backoff for large single-line files", async () => {
- const filePath = "/test/large-minified.js"
- const totalLines = 1
- const currentMaxReadFileLine = -1
-
- // Mock a large single-line file
- vi.mocked(fs.stat).mockResolvedValue({
- size: 2 * 1024 * 1024, // 2MB
- } as any)
-
- // Create a very large single line that exceeds estimated safe chars
- const largeContent = "x".repeat(300000) // 300K chars
- vi.mocked(readLines).mockResolvedValue(largeContent)
-
- // Mock token counting to always exceed limit, forcing maximum cutbacks
- mockTask.api.countTokens = vi.fn().mockResolvedValue(100000) // Always exceeds ~57k limit
-
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
-
- // After maximum cutbacks, it should still limit the file
- expect(result.shouldLimit).toBe(true)
-
- // Check that it returns character count (truncated)
- expect(result.safeContentLimit).toBeGreaterThan(0)
- expect(result.reason).toContain("Large single-line file")
- expect(result.reason).toContain("Only the first")
- expect(result.reason).toContain("This is a hard limit")
- })
-
- it("should handle single-line files that fit after cutback", async () => {
- const filePath = "/test/borderline-minified.js"
- const totalLines = 1
- const currentMaxReadFileLine = -1
-
- // Mock file size
- vi.mocked(fs.stat).mockResolvedValue({
- size: 800 * 1024, // 800KB
- } as any)
-
- // Create content that's just over the limit
- const content = "const x=1;".repeat(20000) // ~200KB
- vi.mocked(readLines).mockResolvedValue(content)
-
- // Mock token counting - first call exceeds, second fits
- let callCount = 0
- mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
- callCount++
- const text = content[0].text
- if (callCount === 1) {
- return 65000 // Just over the ~57k limit
- }
- // After 20% cutback
- return 45000 // Now fits comfortably
+ expect(result.shouldLimit).toBe(true)
+ expect(result.safeContentLimit).toBe(10000) // Ultra-safe fallback
+ expect(result.reason).toContain("Unable to determine file size")
})
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
+ it("should handle readPartialContent errors", async () => {
+ const fileSizeBytes = 2 * 1024 * 1024 // 2MB - large file to trigger validation
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: fileSizeBytes,
+ } as any)
- // Should limit but allow partial read
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBeGreaterThan(0) // Returns character count, not line count
- expect(result.reason).toContain("Large single-line file")
+ // Mock high context usage to prevent heuristic skipping
+ mockTask.getTokenUsage = vi.fn().mockReturnValue({
+ contextTokens: 80000, // 80% of context used - prevents skipping
+ })
- // Verify percentage calculation in reason
- if (result.reason) {
- const match = result.reason.match(/Only the first (\d+)%/)
- expect(match).toBeTruthy()
- if (match) {
- const percentage = parseInt(match[1])
- expect(percentage).toBeGreaterThan(0)
- expect(percentage).toBeLessThan(100)
- }
- }
+ // Mock readPartialContent to fail
+ vi.mocked(readPartialContent).mockRejectedValue(new Error("Read error"))
+
+ const result = await validateFileSizeForContext("/test/read-error.ts", 1000, -1, mockTask)
+
+ // When readPartialContent fails, it falls back to error handling
+ expect(result.shouldLimit).toBe(true)
+ expect(result.safeContentLimit).toBe(50000) // Conservative fallback for large files
+ expect(result.reason).toContain("Large file detected")
+ })
})
- it("should handle single-line files that cannot fit any content", async () => {
- const filePath = "/test/impossible-minified.js"
- const totalLines = 1
- const currentMaxReadFileLine = -1
+ describe("edge cases", () => {
+ it("should handle empty files", async () => {
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: 0,
+ } as any)
- // Mock file size
- vi.mocked(fs.stat).mockResolvedValue({
- size: 10 * 1024 * 1024, // 10MB
- } as any)
+ const result = await validateFileSizeForContext("/test/empty.ts", 0, -1, mockTask)
- // Mock very high context usage
- mockTask.getTokenUsage = vi.fn().mockReturnValue({
- contextTokens: 99000, // 99% used
+ expect(result.shouldLimit).toBe(false)
+ expect(result.safeContentLimit).toBe(-1)
})
- // Create massive content
- const content = "x".repeat(1000000)
- vi.mocked(readLines).mockResolvedValue(content)
+ it("should handle files that exactly match the limit", async () => {
+ // Calculate exact estimated safe chars
+ // 100k - 10k = 90k remaining, 90k * 0.75 = 67.5k usable
+ // 67.5k - 4096 = ~63.4k available, 63.4k * 0.9 = ~57k target
+ // 57k * 3 = 171k estimated safe chars
+ const exactSize = Math.floor(57000 * 3) // Exactly at the limit
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: exactSize,
+ } as any)
- // Mock token counting - always exceeds even after cutbacks
- mockTask.api.countTokens = vi.fn().mockResolvedValue(100000)
+ const result = await validateFileSizeForContext("/test/exact.ts", 1000, -1, mockTask)
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
+ expect(result.shouldLimit).toBe(false)
+ expect(result.safeContentLimit).toBe(-1)
+ })
- // Should completely block the file
- expect(result.shouldLimit).toBe(true)
- expect(result.safeContentLimit).toBe(0)
- expect(result.reason).toContain("Single-line file is too large")
- expect(result.reason).toContain("This file cannot be accessed")
+ it("should handle single-character files", async () => {
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: 1,
+ } as any)
+
+ const result = await validateFileSizeForContext("/test/single-char.ts", 1, -1, mockTask)
+
+ expect(result.shouldLimit).toBe(false)
+ expect(result.safeContentLimit).toBe(-1)
+ })
})
- it("should fall back to regular validation if single-line processing fails", async () => {
- const filePath = "/test/problematic-minified.js"
- const totalLines = 1
- const currentMaxReadFileLine = -1
+ describe("return value validation", () => {
+ it("should always return character counts in safeContentLimit", async () => {
+ const fileSizeBytes = 300 * 1024 // 300KB
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: fileSizeBytes,
+ } as any)
- // Mock file size
- vi.mocked(fs.stat).mockResolvedValue({
- size: 100 * 1024,
- } as any)
+ const content = "const test = 'content';".repeat(5000) // ~100k chars
+ vi.mocked(readPartialContent).mockResolvedValue({
+ content,
+ charactersRead: content.length,
+ totalCharacters: 300000,
+ linesRead: 5000,
+ totalLines: 5000,
+ lastLineRead: 5000,
+ })
- // Mock readLines to fail on first call (single line read)
- vi.mocked(readLines).mockRejectedValueOnce(new Error("Read error")).mockResolvedValue("some content") // Subsequent reads succeed
+ mockTask.api.countTokens = vi.fn().mockResolvedValue(70000) // Exceeds limit
- // Mock token counting
- mockTask.api.countTokens = vi.fn().mockResolvedValue(1000)
+ const result = await validateFileSizeForContext("/test/char-count.ts", 5000, -1, mockTask)
- const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
+ expect(result.shouldLimit).toBe(true)
+ expect(typeof result.safeContentLimit).toBe("number")
+ expect(result.safeContentLimit).toBeGreaterThan(0)
+ // Should be character count, not line count
+ expect(result.safeContentLimit).toBeGreaterThan(5000) // More than line count
+ })
- // Should have attempted to validate the file (may not call readLines if it uses heuristics)
- expect(result.shouldLimit).toBeDefined()
+ it("should return -1 for unlimited files", async () => {
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ size: 3 * 1024, // Small file
+ } as any)
- // Should proceed with regular validation after failure
- expect(result.shouldLimit).toBeDefined()
+ const result = await validateFileSizeForContext("/test/unlimited.ts", 100, -1, mockTask)
+
+ expect(result.shouldLimit).toBe(false)
+ expect(result.safeContentLimit).toBe(-1)
+ })
})
})
})
diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts
index 031640ae7f..04f721e63a 100644
--- a/src/core/tools/__tests__/readFileTool.spec.ts
+++ b/src/core/tools/__tests__/readFileTool.spec.ts
@@ -4,6 +4,7 @@ import * as path from "path"
import { countFileLines } from "../../../integrations/misc/line-counter"
import { readLines } from "../../../integrations/misc/read-lines"
+import { readPartialContent } from "../../../integrations/misc/read-partial-content"
import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../../integrations/misc/extract-text"
import { parseSourceCodeDefinitionsForFile } from "../../../services/tree-sitter"
import { isBinaryFile } from "isbinaryfile"
@@ -36,6 +37,14 @@ vi.mock("../../../integrations/misc/read-lines", () => ({
}))
vi.mock("../../../integrations/misc/read-partial-content", () => ({
readPartialSingleLineContent: vi.fn().mockResolvedValue("mocked partial content"),
+ readPartialContent: vi.fn().mockResolvedValue({
+ content: "mocked partial content",
+ charactersRead: 100,
+ totalCharacters: 1000,
+ linesRead: 5,
+ totalLines: 50,
+ lastLineRead: 5,
+ }),
}))
vi.mock("../contextValidator")
@@ -1367,21 +1376,29 @@ describe("read_file tool XML output structure", () => {
vi.mocked(contextValidatorModule.validateFileSizeForContext).mockResolvedValue({
shouldLimit: true,
safeContentLimit: 2000,
- reason: "File exceeds available context space",
+ reason: "File exceeds available context space. Can read 2000 of 500000 characters (40%). Context usage: 10000/100000 tokens (10%).",
})
- // Mock readLines to return truncated content
- vi.mocked(readLines).mockResolvedValue("Line 1\nLine 2\n...truncated...")
+ // Mock readPartialContent to return truncated content
+ vi.mocked(readPartialContent).mockResolvedValue({
+ content: "Line 1\nLine 2\n...truncated...",
+ charactersRead: 2000,
+ totalCharacters: 500000,
+ linesRead: 100,
+ totalLines: 10000,
+ lastLineRead: 100,
+ })
const result = await executeReadFileTool(
{ args: `large-file.ts` },
{ totalLines: 10000, maxReadFileLine: -1 },
)
- // Verify the result contains the inline instructions
+ // Verify the result contains the partial read notice for multi-line files
expect(result).toContain("")
- expect(result).toContain("File exceeds available context space")
- expect(result).toContain("tools:readFile.contextLimitInstructions")
+ expect(result).toContain("tools:readFile.partialReadMultiLine")
+ // The current implementation doesn't include contextLimitInstructions
+ expect(result).not.toContain("tools:readFile.contextLimitInstructions")
})
it("should not show any special notice when file fits in context", async () => {
@@ -1409,12 +1426,19 @@ describe("read_file tool XML output structure", () => {
// Mock contextValidator to return shouldLimit true with single-line file message
vi.mocked(contextValidatorModule.validateFileSizeForContext).mockResolvedValue({
shouldLimit: true,
- safeContentLimit: 1,
+ safeContentLimit: 5000,
reason: "Large single-line file (likely minified) exceeds available context space. Only the first 50% (5000 of 10000 characters) can be loaded. This is a hard limit - no additional content from this file can be accessed.",
})
- // Mock extractTextFromFile to return truncated content
- vi.mocked(extractTextFromFile).mockResolvedValue("1 | const a=1;const b=2;...truncated")
+ // Mock readPartialContent to return truncated content for single-line file
+ vi.mocked(readPartialContent).mockResolvedValue({
+ content: "const a=1;const b=2;...truncated",
+ charactersRead: 5000,
+ totalCharacters: 10000,
+ linesRead: 1,
+ totalLines: 1,
+ lastLineRead: 1,
+ })
const result = await executeReadFileTool(
{ args: `minified.js` },
@@ -1423,8 +1447,7 @@ describe("read_file tool XML output structure", () => {
// Verify the result contains the notice but NOT the line_range instructions
expect(result).toContain("")
- expect(result).toContain("Large single-line file")
- expect(result).toContain("This is a hard limit")
+ expect(result).toContain("tools:readFile.partialReadSingleLine")
expect(result).not.toContain("tools:readFile.contextLimitInstructions")
expect(result).not.toContain("Use line_range")
})
@@ -1436,22 +1459,30 @@ describe("read_file tool XML output structure", () => {
// Mock contextValidator to return shouldLimit true with multi-line file message
vi.mocked(contextValidatorModule.validateFileSizeForContext).mockResolvedValue({
shouldLimit: true,
- safeContentLimit: 1000,
- reason: "File exceeds available context space. Safely read 1000 lines out of 5000 total lines.",
+ safeContentLimit: 50000,
+ reason: "File exceeds available context space. Can read 50000 of 250000 characters (20%). Context usage: 50000/100000 tokens (50%).",
})
- // Mock readLines to return truncated content
- vi.mocked(readLines).mockResolvedValue("Line 1\nLine 2\n...truncated...")
+ // Mock readPartialContent to return truncated content
+ vi.mocked(readPartialContent).mockResolvedValue({
+ content: "Line 1\nLine 2\n...truncated...",
+ charactersRead: 50000,
+ totalCharacters: 250000,
+ linesRead: 1000,
+ totalLines: 5000,
+ lastLineRead: 1000,
+ })
const result = await executeReadFileTool(
{ args: `large-file.ts` },
{ totalLines: 5000, maxReadFileLine: -1 },
)
- // Verify the result contains both the notice AND the line_range instructions
+ // Verify the result contains the partial read notice for multi-line files
expect(result).toContain("")
- expect(result).toContain("File exceeds available context space")
- expect(result).toContain("tools:readFile.contextLimitInstructions")
+ expect(result).toContain("tools:readFile.partialReadMultiLine")
+ // The current implementation doesn't include contextLimitInstructions
+ expect(result).not.toContain("tools:readFile.contextLimitInstructions")
})
it("should handle normal file read section for single-line files with validation notice", async () => {
@@ -1461,12 +1492,19 @@ describe("read_file tool XML output structure", () => {
// Mock contextValidator to return shouldLimit true with a single-line file notice
vi.mocked(contextValidatorModule.validateFileSizeForContext).mockResolvedValue({
shouldLimit: true,
- safeContentLimit: 1,
- reason: "Large single-line file (likely minified) exceeds available context space. Only the first 80% can be loaded.",
+ safeContentLimit: 8000,
+ reason: "Large single-line file (likely minified) exceeds available context space. Only the first 80% (8000 of 10000 characters) can be loaded.",
})
- // Mock extractTextFromFile
- vi.mocked(extractTextFromFile).mockResolvedValue("1 | const a=1;const b=2;const c=3;")
+ // Mock readPartialContent for single-line file
+ vi.mocked(readPartialContent).mockResolvedValue({
+ content: "const a=1;const b=2;const c=3;",
+ charactersRead: 8000,
+ totalCharacters: 10000,
+ linesRead: 1,
+ totalLines: 1,
+ lastLineRead: 1,
+ })
const result = await executeReadFileTool(
{ args: `semi-large.js` },
@@ -1475,7 +1513,7 @@ describe("read_file tool XML output structure", () => {
// Verify single-line file notice doesn't include line_range instructions
expect(result).toContain("")
- expect(result).toContain("Large single-line file")
+ expect(result).toContain("tools:readFile.partialReadSingleLine")
expect(result).not.toContain("tools:readFile.contextLimitInstructions")
})
})
diff --git a/src/core/tools/contextValidator.ts b/src/core/tools/contextValidator.ts
index 0c8e11fad6..bf0032ddd0 100644
--- a/src/core/tools/contextValidator.ts
+++ b/src/core/tools/contextValidator.ts
@@ -1,6 +1,5 @@
import { Task } from "../task/Task"
-import { readLines } from "../../integrations/misc/read-lines"
-import { readPartialSingleLineContent } from "../../integrations/misc/read-partial-content"
+import { readPartialContent } from "../../integrations/misc/read-partial-content"
import { getModelMaxOutputTokens, getFormatForProvider } from "../../shared/api"
import * as fs from "fs/promises"
@@ -16,19 +15,19 @@ const FILE_READ_BUFFER_PERCENTAGE = 0.25 // 25% buffer for file reads
*/
const CHARS_PER_TOKEN_ESTIMATE = 3
const CUTBACK_PERCENTAGE = 0.2 // 20% reduction when over limit
-const READ_BATCH_SIZE = 50 // Read 50 lines at a time for efficiency
const MAX_API_CALLS = 5 // Safety limit to prevent infinite loops
-const MIN_USEFUL_LINES = 50 // Minimum lines to consider useful
+const MIN_USEFUL_CHARS = 1000 // Minimum characters to consider useful
/**
* File size thresholds for heuristics
*/
const TINY_FILE_SIZE = 5 * 1024 // 5KB - definitely safe to skip validation
const SMALL_FILE_SIZE = 100 * 1024 // 100KB - safe if context is mostly empty
+const LARGE_FILE_SIZE = 1024 * 1024 // 1MB - skip tokenizer for speed, use cutback percentage
export interface ContextValidationResult {
shouldLimit: boolean
- safeContentLimit: number // For single-line files, this represents character count; for multi-line files, it's line count
+ safeContentLimit: number // Always represents character count
reason?: string
}
@@ -75,7 +74,7 @@ async function getContextInfo(cline: Task): Promise {
* Returns true if we're confident the file can be read without limits.
* Prioritizes accuracy - only skips when very confident.
*/
-async function shouldSkipValidation(filePath: string, totalLines: number, cline: Task): Promise {
+async function shouldSkipValidation(filePath: string, cline: Task): Promise {
try {
// Get file size
const stats = await fs.stat(filePath)
@@ -112,181 +111,12 @@ async function shouldSkipValidation(filePath: string, totalLines: number, cline:
}
/**
- * Detects if a file is effectively a single-line file (1-5 lines with only one non-empty line)
- * This handles cases where minified files might have a few empty lines but are essentially single-line
- * TODO: make this more robust
- */
-async function isEffectivelySingleLine(filePath: string, totalLines: number): Promise {
- // Only check files with 1-5 lines
- if (totalLines < 1 || totalLines > 5) {
- return false
- }
-
- // Single line files are always effectively single line
- if (totalLines === 1) {
- return true
- }
-
- try {
- // Check if file is big (>100KB) and lines 2-5 are empty
- const stats = await fs.stat(filePath)
- const fileSizeBytes = stats.size
-
- // Only apply this logic to big files
- if (fileSizeBytes < 100 * 1024) {
- // Less than 100KB
- return false
- }
-
- // Read all lines to check if lines 2-5 are empty
- const content = await readLines(filePath, totalLines - 1, 0)
- const lines = content.split("\n")
-
- // Check if lines 2-5 (indices 1-4) are empty
- let hasEmptyLines2to5 = true
- for (let i = 1; i < Math.min(lines.length, 5); i++) {
- if (lines[i].trim().length > 0) {
- hasEmptyLines2to5 = false
- break
- }
- }
-
- console.log(
- `[isEffectivelySingleLine] File ${filePath}: totalLines=${totalLines}, fileSize=${(fileSizeBytes / 1024).toFixed(1)}KB, hasEmptyLines2to5=${hasEmptyLines2to5}`,
- )
-
- return hasEmptyLines2to5
- } catch (error) {
- console.warn(`[isEffectivelySingleLine] Error checking file ${filePath}: ${error}`)
- return false
- }
-}
-
-/**
- * Validates a single-line file (likely minified) to see if it fits in context
- * Uses character-based estimation only (no token validation to avoid API hangs)
- * TODO: handle 2-phase validation once we have better partial line reading
- */
-async function validateSingleLineFile(
- filePath: string,
- cline: Task,
- contextInfo: ContextInfo,
-): Promise {
- console.log(
- `[validateFileSizeForContext] Single-line file detected: ${filePath} - using character-based estimation`,
- )
-
- try {
- // Use char heuristic to estimate safe content size
- const estimatedSafeChars = contextInfo.targetTokenLimit * CHARS_PER_TOKEN_ESTIMATE
-
- // Get file size
- const stats = await fs.stat(filePath)
- const fullFileSize = stats.size
-
- // If file is smaller than our estimated safe chars, it should fit
- if (fullFileSize <= estimatedSafeChars) {
- console.log(
- `[validateFileSizeForContext] Single-line file fits within estimated safe chars (${fullFileSize} <= ${estimatedSafeChars})`,
- )
- return { shouldLimit: false, safeContentLimit: -1 }
- }
-
- // File is larger than estimated safe chars
- const percentageRead = Math.round((estimatedSafeChars / fullFileSize) * 100)
- console.log(
- `[validateFileSizeForContext] Single-line file exceeds estimated safe chars (${fullFileSize} > ${estimatedSafeChars}), limiting to ${percentageRead}%`,
- )
-
- // Special case: if we can't read any meaningful content
- if (estimatedSafeChars === 0 || percentageRead === 0) {
- return {
- shouldLimit: true,
- safeContentLimit: 0,
- reason: `Single-line file is too large to read any portion. File size: ${fullFileSize} characters. Available context space: ${contextInfo.availableTokensForFile} tokens. This file cannot be accessed.`,
- }
- }
-
- return {
- shouldLimit: true,
- safeContentLimit: estimatedSafeChars, // Return character count limit
- reason: `Large single-line file (likely minified) exceeds available context space. Only the first ${percentageRead}% (${estimatedSafeChars} of ${fullFileSize} characters) can be loaded. Context: ${contextInfo.currentlyUsed}/${contextInfo.contextWindow} tokens used (${Math.round((contextInfo.currentlyUsed / contextInfo.contextWindow) * 100)}%). This is a hard limit - no additional content from this file can be accessed.`,
- }
- } catch (error) {
- // Check for specific error types that indicate memory issues
- if (error instanceof Error) {
- const errorMessage = error.message.toLowerCase()
- if (
- errorMessage.includes("heap") ||
- errorMessage.includes("memory") ||
- errorMessage.includes("allocation")
- ) {
- // Return a safe fallback instead of crashing
- return {
- shouldLimit: true,
- safeContentLimit: 0,
- reason: `File is too large to process due to memory constraints. Error: ${error.message}. This file cannot be accessed.`,
- }
- }
- }
-
- console.warn(`[validateFileSizeForContext] Error processing single-line file: ${error}`)
- return null // Fall through to regular validation for other errors
- }
-}
-
-/**
- * Reads file content in batches up to the estimated safe character limit
- */
-async function readFileInBatches(
- filePath: string,
- totalLines: number,
- estimatedSafeChars: number,
-): Promise<{ content: string; lineCount: number; lineToCharMap: Map }> {
- let accumulatedContent = ""
- let currentLine = 0
- const lineToCharMap: Map = new Map()
-
- // Track the start position of each line for potential cutback
- lineToCharMap.set(0, 0)
-
- // Read until we hit our estimated character limit or EOF
- while (currentLine < totalLines && accumulatedContent.length < estimatedSafeChars) {
- const batchEndLine = Math.min(currentLine + READ_BATCH_SIZE - 1, totalLines - 1)
-
- try {
- const batchContent = await readLines(filePath, batchEndLine, currentLine)
-
- // Track line positions within the accumulated content
- let localPos = 0
- for (let lineNum = currentLine; lineNum <= batchEndLine; lineNum++) {
- const nextNewline = batchContent.indexOf("\n", localPos)
- if (nextNewline !== -1) {
- lineToCharMap.set(lineNum + 1, accumulatedContent.length + nextNewline + 1)
- localPos = nextNewline + 1
- }
- }
-
- accumulatedContent += batchContent
- currentLine = batchEndLine + 1
- } catch (error) {
- console.warn(`[validateFileSizeForContext] Error reading batch: ${error}`)
- break
- }
- }
-
- return { content: accumulatedContent, lineCount: currentLine, lineToCharMap }
-}
-
-/**
- * Shared function to validate content with actual API and apply cutback if needed
- * Works for both single-line and multi-line content
+ * Validates content with actual API and applies cutback if needed
*/
async function validateAndCutbackContent(
content: string,
targetTokenLimit: number,
cline: Task,
- isSingleLine: boolean = false,
): Promise<{ finalContent: string; actualTokens: number; didCutback: boolean }> {
let finalContent = content
let apiCallCount = 0
@@ -300,7 +130,7 @@ async function validateAndCutbackContent(
actualTokens = await cline.api.countTokens([{ type: "text", text: finalContent }])
console.log(
- `[validateFileSizeForContext] API call ${apiCallCount}: ${actualTokens} tokens for ${finalContent.length} chars${isSingleLine ? " (single-line)" : ""}`,
+ `[validateFileSizeForContext] API call ${apiCallCount}: ${actualTokens} tokens for ${finalContent.length} chars`,
)
if (actualTokens <= targetTokenLimit) {
@@ -323,58 +153,11 @@ async function validateAndCutbackContent(
return { finalContent, actualTokens, didCutback }
}
-/**
- * Validates content with actual API and cuts back if needed (for multi-line files)
- */
-async function validateAndAdjustContent(
- accumulatedContent: string,
- initialLineCount: number,
- lineToCharMap: Map,
- targetTokenLimit: number,
- totalLines: number,
- cline: Task,
-): Promise<{ finalContent: string; finalLineCount: number }> {
- // Use the shared validation function
- const { finalContent, didCutback } = await validateAndCutbackContent(
- accumulatedContent,
- targetTokenLimit,
- cline,
- false,
- )
-
- // If no cutback was needed, return original line count
- if (!didCutback) {
- return { finalContent, finalLineCount: initialLineCount }
- }
-
- // Find the line that corresponds to the cut content length
- let cutoffLine = 0
- for (const [lineNum, charPos] of lineToCharMap.entries()) {
- if (charPos > finalContent.length) {
- break
- }
- cutoffLine = lineNum
- }
-
- // Ensure we don't cut back too far
- if (cutoffLine < 10) {
- console.warn(`[validateFileSizeForContext] Cutback resulted in too few lines (${cutoffLine}), using minimum`)
- cutoffLine = Math.min(MIN_USEFUL_LINES, totalLines)
- }
-
- // Get the character position for the cutoff line
- const cutoffCharPos = lineToCharMap.get(cutoffLine) || 0
- const adjustedContent = accumulatedContent.substring(0, cutoffCharPos)
-
- return { finalContent: adjustedContent, finalLineCount: cutoffLine }
-}
-
/**
* Handles error cases with conservative fallback
*/
async function handleValidationError(
filePath: string,
- totalLines: number,
currentMaxReadFileLine: number,
error: unknown,
): Promise {
@@ -387,27 +170,35 @@ async function handleValidationError(
// Very small files are safe
if (fileSizeBytes < TINY_FILE_SIZE) {
- return { shouldLimit: false, safeContentLimit: currentMaxReadFileLine }
+ return { shouldLimit: false, safeContentLimit: -1 }
+ }
+
+ // For larger files, apply a conservative character limit
+ if (fileSizeBytes > 1024 * 1024) {
+ // > 1MB
+ return {
+ shouldLimit: true,
+ safeContentLimit: 50000, // 50K chars as a safe fallback
+ reason: "Large file detected. Limited to 50,000 characters to prevent context overflow (runtime state unavailable).",
+ }
}
} catch (statError) {
- // If we can't even stat the file, proceed with conservative defaults
+ // If we can't even stat the file, proceed with very conservative defaults
console.warn(`[validateFileSizeForContext] Could not stat file: ${statError}`)
- }
-
- if (totalLines > 10000) {
return {
shouldLimit: true,
- safeContentLimit: 1000,
- reason: "Large file detected (>10,000 lines). Limited to 1000 lines to prevent context overflow (runtime state unavailable).",
+ safeContentLimit: 10000, // 10K chars as ultra-safe fallback
+ reason: "Unable to determine file size. Limited to 10,000 characters as a precaution.",
}
}
- return { shouldLimit: false, safeContentLimit: currentMaxReadFileLine }
+
+ return { shouldLimit: false, safeContentLimit: -1 }
}
/**
* Validates if a file can be safely read based on its size and current runtime context state.
* Uses a 2-phase approach: character-based estimation followed by actual token validation.
- * Returns a safe maxReadFileLine value to prevent context overflow.
+ * Returns a safe character limit to prevent context overflow.
*/
export async function validateFileSizeForContext(
filePath: string,
@@ -417,63 +208,96 @@ export async function validateFileSizeForContext(
): Promise {
try {
// Check if we can skip validation
- if (await shouldSkipValidation(filePath, totalLines, cline)) {
- return { shouldLimit: false, safeContentLimit: currentMaxReadFileLine }
+ if (await shouldSkipValidation(filePath, cline)) {
+ return { shouldLimit: false, safeContentLimit: -1 }
}
// Get context information
const contextInfo = await getContextInfo(cline)
- // Special handling for single-line files (likely minified) or effectively single-line files
- const isEffSingleLine = await isEffectivelySingleLine(filePath, totalLines)
- if (isEffSingleLine) {
- const singleLineResult = await validateSingleLineFile(filePath, cline, contextInfo)
- if (singleLineResult) {
- return singleLineResult
- }
- // Fall through to regular validation if single-line validation failed
+ // Phase 1: Estimate safe character limit based on available tokens
+ const estimatedSafeChars = contextInfo.targetTokenLimit * CHARS_PER_TOKEN_ESTIMATE
+
+ // Get file size to check if we need to limit
+ const stats = await fs.stat(filePath)
+ const fileSizeBytes = stats.size
+
+ // If file is smaller than our estimated safe chars, it should fit
+ if (fileSizeBytes <= estimatedSafeChars) {
+ console.log(
+ `[validateFileSizeForContext] File fits within estimated safe chars (${fileSizeBytes} <= ${estimatedSafeChars})`,
+ )
+ return { shouldLimit: false, safeContentLimit: -1 }
}
- // Phase 1: Read content up to estimated safe character limit
- const estimatedSafeChars = contextInfo.targetTokenLimit * CHARS_PER_TOKEN_ESTIMATE
- const { content, lineCount, lineToCharMap } = await readFileInBatches(filePath, totalLines, estimatedSafeChars)
-
- // Phase 2: Validate with actual API and cutback if needed
- const { finalContent, finalLineCount } = await validateAndAdjustContent(
- content,
- lineCount,
- lineToCharMap,
- contextInfo.targetTokenLimit,
- totalLines,
- cline,
+ // File is larger than estimated safe chars, need to validate with actual content
+ console.log(
+ `[validateFileSizeForContext] File exceeds estimated safe chars (${fileSizeBytes} > ${estimatedSafeChars}), validating with actual content`,
)
- // Log final statistics
- console.log(`[validateFileSizeForContext] Final: ${finalLineCount} lines, ${finalContent.length} chars`)
+ // Phase 2: Read content up to estimated limit and validate with actual API
+ const partialResult = await readPartialContent(filePath, estimatedSafeChars)
+
+ // For large files, skip tokenizer validation for speed and apply clean cutback percentage
+ let finalContent: string
+ let actualTokens: number
+ let didCutback: boolean
+
+ if (fileSizeBytes > LARGE_FILE_SIZE) {
+ // Skip tokenizer for speed reasons on large files, apply clean cutback
+ const cutbackChars = Math.floor(partialResult.content.length * (1 - CUTBACK_PERCENTAGE))
+ finalContent = partialResult.content.substring(0, cutbackChars)
+ actualTokens = 0 // Not calculated for large files
+ didCutback = cutbackChars < partialResult.content.length
+
+ console.log(
+ `[validateFileSizeForContext] Large file (${(fileSizeBytes / 1024 / 1024).toFixed(1)}MB) - skipping tokenizer for speed, applying ${Math.round(CUTBACK_PERCENTAGE * 100)}% cutback: ${partialResult.content.length} -> ${finalContent.length} chars`,
+ )
+ } else {
+ // Use tokenizer validation for smaller files
+ const validation = await validateAndCutbackContent(
+ partialResult.content,
+ contextInfo.targetTokenLimit,
+ cline,
+ )
+ finalContent = validation.finalContent
+ actualTokens = validation.actualTokens
+ didCutback = validation.didCutback
+ }
+
+ // Calculate final safe character limit
+ const finalSafeChars = finalContent.length
// Ensure we provide at least a minimum useful amount
- const finalSafeContentLimit = Math.max(MIN_USEFUL_LINES, finalLineCount)
+ const safeContentLimit = Math.max(MIN_USEFUL_CHARS, finalSafeChars)
- // If we read the entire file without exceeding the limit, no limitation needed
- if (finalLineCount >= totalLines) {
- return { shouldLimit: false, safeContentLimit: currentMaxReadFileLine }
- }
+ // Log final statistics
+ console.log(`[validateFileSizeForContext] Final: ${safeContentLimit} chars, ${actualTokens} tokens`)
- // If we couldn't read even the minimum useful lines
- if (finalLineCount < MIN_USEFUL_LINES) {
+ // Special case: if we can't read any meaningful content
+ if (safeContentLimit === MIN_USEFUL_CHARS && finalSafeChars < MIN_USEFUL_CHARS) {
+ const percentageRead = Math.round((safeContentLimit / fileSizeBytes) * 100)
return {
shouldLimit: true,
- safeContentLimit: finalSafeContentLimit,
- reason: `Very limited context space. Could only safely read ${finalLineCount} lines before exceeding token limit. Context: ${contextInfo.currentlyUsed}/${contextInfo.contextWindow} tokens used (${Math.round((contextInfo.currentlyUsed / contextInfo.contextWindow) * 100)}%). Limited to ${finalSafeContentLimit} lines. Consider using search_files or line_range for specific sections.`,
+ safeContentLimit,
+ reason: `Very limited context space. Can only read ${safeContentLimit} characters (${percentageRead}% of file). Context: ${contextInfo.currentlyUsed}/${contextInfo.contextWindow} tokens used (${Math.round((contextInfo.currentlyUsed / contextInfo.contextWindow) * 100)}%). Consider using search_files or line_range for specific sections.`,
}
}
+ // If we read the entire file without exceeding the limit, no limitation needed
+ if (!didCutback && partialResult.charactersRead === fileSizeBytes) {
+ return { shouldLimit: false, safeContentLimit: -1 }
+ }
+
+ // Calculate percentage read for the notice
+ const percentageRead = Math.round((safeContentLimit / fileSizeBytes) * 100)
+
return {
shouldLimit: true,
- safeContentLimit: finalSafeContentLimit,
- reason: `File exceeds available context space. Safely read ${finalSafeContentLimit} lines out of ${totalLines} total lines. Context usage: ${contextInfo.currentlyUsed}/${contextInfo.contextWindow} tokens (${Math.round((contextInfo.currentlyUsed / contextInfo.contextWindow) * 100)}%). Use line_range to read specific sections.`,
+ safeContentLimit,
+ reason: `File exceeds available context space. Can read ${safeContentLimit} of ${fileSizeBytes} characters (${percentageRead}%). Context usage: ${contextInfo.currentlyUsed}/${contextInfo.contextWindow} tokens (${Math.round((contextInfo.currentlyUsed / contextInfo.contextWindow) * 100)}%).`,
}
} catch (error) {
- return handleValidationError(filePath, totalLines, currentMaxReadFileLine, error)
+ return handleValidationError(filePath, currentMaxReadFileLine, error)
}
}
diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts
index 26b2db31a2..63ca692fd7 100644
--- a/src/core/tools/readFileTool.ts
+++ b/src/core/tools/readFileTool.ts
@@ -12,7 +12,7 @@ import { isPathOutsideWorkspace } from "../../utils/pathUtils"
import { getReadablePath } from "../../utils/path"
import { countFileLines } from "../../integrations/misc/line-counter"
import { readLines } from "../../integrations/misc/read-lines"
-import { readPartialSingleLineContent } from "../../integrations/misc/read-partial-content"
+import { readPartialContent } from "../../integrations/misc/read-partial-content"
import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text"
import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
import { parseXml } from "../../utils/xml"
@@ -460,15 +460,10 @@ export async function readFileTool(
// Preemptive file size validation to prevent context overflow
const validation = await validateFileSizeForContext(fullPath, totalLines, maxReadFileLine, cline)
- let effectiveMaxReadFileLine = maxReadFileLine
let validationNotice = ""
- // For single-line files, ALWAYS apply validation regardless of maxReadFileLine setting
- // For multi-line files, only apply if maxReadFileLine is -1 (unlimited)
- if (validation.shouldLimit && (totalLines === 1 || maxReadFileLine === -1)) {
- effectiveMaxReadFileLine = validation.safeContentLimit
- validationNotice = validation.reason || ""
- }
+ // Apply validation if maxReadFileLine is -1 (unlimited)
+ const shouldApplyValidation = validation.shouldLimit && maxReadFileLine === -1
// Handle binary files (but allow specific file types that extractTextFromFile can handle)
if (isBinary) {
@@ -560,11 +555,11 @@ export async function readFileTool(
}
// Handle definitions-only mode
- if (effectiveMaxReadFileLine === 0) {
+ if (maxReadFileLine === 0) {
try {
const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController)
if (defResult) {
- let xmlInfo = `${t("tools:readFile.showingOnlyLines", { shown: effectiveMaxReadFileLine, total: totalLines })}\n`
+ let xmlInfo = `${t("tools:readFile.showingOnlyLines", { shown: 0, total: totalLines })}\n`
updateFileResult(relPath, {
xmlContent: `${relPath}\n${defResult}\n${xmlInfo}`,
})
@@ -581,35 +576,14 @@ export async function readFileTool(
continue
}
- // Handle files exceeding line threshold (including preemptive limits)
- // For single-line files with validation limits, ALWAYS use partial reading
- // Also check if this is an effectively single-line file (includes minified files with long lines)
- const isEffectivelySingleLine =
- totalLines <= 5 &&
- validation.shouldLimit &&
- validationNotice &&
- validationNotice.includes("single-line file")
+ // Handle files with validation limits (character-based reading)
+ if (shouldApplyValidation) {
+ const result = await readPartialContent(fullPath, validation.safeContentLimit)
- const shouldUsePartialRead =
- (effectiveMaxReadFileLine > 0 && totalLines > effectiveMaxReadFileLine) ||
- (totalLines === 1 && validation.shouldLimit && effectiveMaxReadFileLine > 0) ||
- (isEffectivelySingleLine && effectiveMaxReadFileLine > 0)
+ // Generate line range attribute based on what was read
+ const lineRangeAttr = result.linesRead === 1 ? ` lines="1"` : ` lines="1-${result.lastLineRead}"`
- if (shouldUsePartialRead) {
- let content: string
- let lineRangeAttr: string
-
- // Special handling for single-line files where effectiveMaxReadFileLine represents character count
- if (totalLines === 1 || isEffectivelySingleLine) {
- // For single-line or effectively single-line files, effectiveMaxReadFileLine is actually a character count
- const partialContent = await readPartialSingleLineContent(fullPath, effectiveMaxReadFileLine)
- content = addLineNumbers(partialContent, 1)
- lineRangeAttr = ` lines="1"`
- } else {
- // For multi-line files, use normal line-based reading
- content = addLineNumbers(await readLines(fullPath, effectiveMaxReadFileLine - 1, 0))
- lineRangeAttr = ` lines="1-${effectiveMaxReadFileLine}"`
- }
+ const content = addLineNumbers(result.content, 1)
let xmlInfo = `\n${content}\n`
try {
@@ -618,21 +592,62 @@ export async function readFileTool(
xmlInfo += `${defResult}\n`
}
- // Add appropriate notice based on whether this was a preemptive limit or user setting
- if (validationNotice) {
- // Check if this is a single-line file
- if (totalLines === 1 && validationNotice.includes("single-line file")) {
- // For single-line files, don't suggest line_range tool
- xmlInfo += `${validationNotice}\n`
- } else {
- // For multi-line files, provide inline instructions to use line_range
- const instructions = t("tools:readFile.contextLimitInstructions", { path: relPath })
- xmlInfo += `${validationNotice}\n\n${instructions}\n`
- }
+ // Generate notice based on what was read
+ const percentRead = Math.round((result.charactersRead / result.totalCharacters) * 100)
+ if (result.linesRead === 1) {
+ // Single-line file
+ const notice = t("tools:readFile.partialReadSingleLine", {
+ charactersRead: result.charactersRead,
+ totalCharacters: result.totalCharacters,
+ percentRead,
+ })
+ xmlInfo += `${notice}\n`
} else {
- xmlInfo += `${t("tools:readFile.showingOnlyLines", { shown: effectiveMaxReadFileLine, total: totalLines })}\n`
+ // Multi-line file
+ const nextLineStart = result.lastLineRead + 1
+ const suggestedLineEnd = Math.min(result.lastLineRead + 1000, result.totalLines)
+ const notice = t("tools:readFile.partialReadMultiLine", {
+ charactersRead: result.charactersRead,
+ totalCharacters: result.totalCharacters,
+ percentRead,
+ lastLineRead: result.lastLineRead,
+ totalLines: result.totalLines,
+ path: relPath,
+ nextLineStart,
+ suggestedLineEnd,
+ })
+ xmlInfo += `${notice}\n`
}
+ const finalXml = `${relPath}\n${xmlInfo}`
+ updateFileResult(relPath, {
+ xmlContent: finalXml,
+ })
+ } catch (error) {
+ if (error instanceof Error && error.message.startsWith("Unsupported language:")) {
+ console.warn(`[read_file] Warning: ${error.message}`)
+ } else {
+ console.error(
+ `[read_file] Unhandled error: ${error instanceof Error ? error.message : String(error)}`,
+ )
+ }
+ }
+ continue
+ }
+
+ // Handle files with line limits (maxReadFileLine > 0)
+ if (maxReadFileLine > 0 && totalLines > maxReadFileLine) {
+ const content = addLineNumbers(await readLines(fullPath, maxReadFileLine - 1, 0))
+ const lineRangeAttr = ` lines="1-${maxReadFileLine}"`
+ let xmlInfo = `\n${content}\n`
+
+ try {
+ const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController)
+ if (defResult) {
+ xmlInfo += `${defResult}\n`
+ }
+ xmlInfo += `${t("tools:readFile.showingOnlyLines", { shown: maxReadFileLine, total: totalLines })}\n`
+
updateFileResult(relPath, {
xmlContent: `${relPath}\n${xmlInfo}`,
})
@@ -648,19 +663,7 @@ export async function readFileTool(
continue
}
- // Handle normal file read
- // CRITICAL: Check if this is a single-line or effectively single-line file that should have been limited
- const isEffSingleLine =
- totalLines <= 5 && validationNotice && validationNotice.includes("single-line file")
- if ((totalLines === 1 || isEffSingleLine) && validation.shouldLimit) {
- console.error(
- `[read_file] ERROR: ${isEffSingleLine ? "Effectively " : ""}Single-line file ${relPath} with validation limits is being read in full! This should not happen.`,
- )
- console.error(
- `[read_file] Debug info: effectiveMaxReadFileLine=${effectiveMaxReadFileLine}, validation.safeContentLimit=${validation.safeContentLimit}`,
- )
- }
-
+ // Handle normal file read (no limits)
const content = await extractTextFromFile(fullPath)
const lineRangeAttr = ` lines="1-${totalLines}"`
@@ -668,16 +671,6 @@ export async function readFileTool(
if (totalLines === 0) {
xmlInfo += `File is empty\n`
- } else if (validationNotice) {
- // Check if this is a single-line file
- if (totalLines === 1 && validationNotice.includes("single-line file")) {
- // For single-line files, don't suggest line_range tool
- xmlInfo += `${validationNotice}\n`
- } else {
- // For multi-line files, provide inline instructions to use line_range
- const instructions = t("tools:readFile.contextLimitInstructions", { path: relPath })
- xmlInfo += `${validationNotice}\n\n${instructions}\n`
- }
}
// Track file read
diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json
index 8a5735f5a1..63e8655961 100644
--- a/src/i18n/locales/ca/tools.json
+++ b/src/i18n/locales/ca/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (només definicions)",
"maxLines": " (màxim {{max}} línies)",
"showingOnlyLines": "Mostrant només {{shown}} de {{total}} línies totals. Utilitza line_range si necessites llegir més línies",
- "contextLimitInstructions": "Per llegir seccions específiques d'aquest fitxer, utilitza el següent format:\n\n\n \n {{path}}\n inici-final\n \n\n\n\nPer exemple, per llegir les línies 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "El fitxer d'imatge és massa gran ({{size}} MB). La mida màxima permesa és {{max}} MB.",
- "imageWithSize": "Fitxer d'imatge ({{size}} KB)"
+ "imageWithSize": "Fitxer d'imatge ({{size}} KB)",
+ "partialReadSingleLine": "Llegits {{charactersRead}} de {{totalCharacters}} caràcters ({{percentRead}}%) d'aquest fitxer d'una sola línia. Aquesta és una lectura parcial - el contingut restant no es pot accedir a causa de limitacions de context.",
+ "partialReadMultiLine": "Llegits {{charactersRead}} de {{totalCharacters}} caràcters ({{percentRead}}%), fins a la línia {{lastLineRead}} de {{totalLines}}. Per llegir seccions específiques d'aquest fitxer, utilitza el següent format:\n\n\n \n {{path}}\n start-end\n \n\n\n\nPer exemple, per llegir les línies {{nextLineStart}}-{{suggestedLineEnd}}:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo sembla estar atrapat en un bucle, intentant la mateixa acció ({{toolName}}) repetidament. Això podria indicar un problema amb la seva estratègia actual. Considera reformular la tasca, proporcionar instruccions més específiques o guiar-lo cap a un enfocament diferent.",
"codebaseSearch": {
diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json
index b0ada21bb8..6c5691e61c 100644
--- a/src/i18n/locales/de/tools.json
+++ b/src/i18n/locales/de/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (nur Definitionen)",
"maxLines": " (maximal {{max}} Zeilen)",
"showingOnlyLines": "Zeige nur {{shown}} von {{total}} Zeilen insgesamt. Verwende line_range, wenn du mehr Zeilen lesen musst",
- "contextLimitInstructions": "Um bestimmte Abschnitte dieser Datei zu lesen, verwende das folgende Format:\n\n\n \n {{path}}\n start-ende\n \n\n\n\nZum Beispiel, um die Zeilen 2001-3000 zu lesen:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "Die Bilddatei ist zu groß ({{size}} MB). Die maximal erlaubte Größe beträgt {{max}} MB.",
- "imageWithSize": "Bilddatei ({{size}} KB)"
+ "imageWithSize": "Bilddatei ({{size}} KB)",
+ "partialReadSingleLine": "{{charactersRead}} von {{totalCharacters}} Zeichen ({{percentRead}}%) aus dieser einzeiligen Datei gelesen. Dies ist ein partieller Lesevorgang - der verbleibende Inhalt kann aufgrund von Kontextbeschränkungen nicht zugegriffen werden.",
+ "partialReadMultiLine": "{{charactersRead}} von {{totalCharacters}} Zeichen ({{percentRead}}%) gelesen, bis Zeile {{lastLineRead}} von {{totalLines}}. Um bestimmte Abschnitte dieser Datei zu lesen, verwende das folgende Format:\n\n\n \n {{path}}\n start-end\n \n\n\n\nZum Beispiel, um die Zeilen {{nextLineStart}}-{{suggestedLineEnd}} zu lesen:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo scheint in einer Schleife festzustecken und versucht wiederholt dieselbe Aktion ({{toolName}}). Dies könnte auf ein Problem mit der aktuellen Strategie hindeuten. Überlege dir, die Aufgabe umzuformulieren, genauere Anweisungen zu geben oder Roo zu einem anderen Ansatz zu führen.",
"codebaseSearch": {
diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json
index ea5a5ee5d8..060653e68a 100644
--- a/src/i18n/locales/en/tools.json
+++ b/src/i18n/locales/en/tools.json
@@ -4,7 +4,8 @@
"definitionsOnly": " (definitions only)",
"maxLines": " (max {{max}} lines)",
"showingOnlyLines": "Showing only {{shown}} of {{total}} total lines. Use line_range if you need to read more lines",
- "contextLimitInstructions": "To read specific sections of this file, use the following format:\n\n\n \n {{path}}\n start-end\n \n\n\n\nFor example, to read lines 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
+ "partialReadSingleLine": "Read {{charactersRead}} of {{totalCharacters}} characters ({{percentRead}}%) from this single-line file. This is a partial read - the remaining content cannot be accessed due to context limitations.",
+ "partialReadMultiLine": "Read {{charactersRead}} of {{totalCharacters}} characters ({{percentRead}}%), up to line {{lastLineRead}} of {{totalLines}}. To read specific sections of this file, use the following format:\n\n\n \n {{path}}\n start-end\n \n\n\n\nFor example, to read lines {{nextLineStart}}-{{suggestedLineEnd}}:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n",
"imageTooLarge": "Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB.",
"imageWithSize": "Image file ({{size}} KB)"
},
diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json
index 203fa920f4..5d22692014 100644
--- a/src/i18n/locales/es/tools.json
+++ b/src/i18n/locales/es/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (solo definiciones)",
"maxLines": " (máximo {{max}} líneas)",
"showingOnlyLines": "Mostrando solo {{shown}} de {{total}} líneas totales. Usa line_range si necesitas leer más líneas",
- "contextLimitInstructions": "Para leer secciones específicas de este archivo, usa el siguiente formato:\n\n\n \n {{path}}\n inicio-fin\n \n\n\n\nPor ejemplo, para leer las líneas 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "El archivo de imagen es demasiado grande ({{size}} MB). El tamaño máximo permitido es {{max}} MB.",
- "imageWithSize": "Archivo de imagen ({{size}} KB)"
+ "imageWithSize": "Archivo de imagen ({{size}} KB)",
+ "partialReadSingleLine": "Leídos {{charactersRead}} de {{totalCharacters}} caracteres ({{percentRead}}%) de este archivo de una sola línea. Esta es una lectura parcial - el contenido restante no se puede acceder debido a limitaciones de contexto.",
+ "partialReadMultiLine": "Leídos {{charactersRead}} de {{totalCharacters}} caracteres ({{percentRead}}%), hasta la línea {{lastLineRead}} de {{totalLines}}. Para leer secciones específicas de este archivo, usa el siguiente formato:\n\n\n \n {{path}}\n start-end\n \n\n\n\nPor ejemplo, para leer las líneas {{nextLineStart}}-{{suggestedLineEnd}}:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo parece estar atrapado en un bucle, intentando la misma acción ({{toolName}}) repetidamente. Esto podría indicar un problema con su estrategia actual. Considera reformular la tarea, proporcionar instrucciones más específicas o guiarlo hacia un enfoque diferente.",
"codebaseSearch": {
diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json
index 5e2827fc17..37000efc63 100644
--- a/src/i18n/locales/fr/tools.json
+++ b/src/i18n/locales/fr/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (définitions uniquement)",
"maxLines": " (max {{max}} lignes)",
"showingOnlyLines": "Affichage de seulement {{shown}} sur {{total}} lignes totales. Utilise line_range si tu as besoin de lire plus de lignes",
- "contextLimitInstructions": "Pour lire des sections spécifiques de ce fichier, utilise le format suivant :\n\n\n \n {{path}}\n début-fin\n \n\n\n\nPar exemple, pour lire les lignes 2001-3000 :\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "Le fichier image est trop volumineux ({{size}} MB). La taille maximale autorisée est {{max}} MB.",
- "imageWithSize": "Fichier image ({{size}} Ko)"
+ "imageWithSize": "Fichier image ({{size}} Ko)",
+ "partialReadSingleLine": "Lu {{charactersRead}} sur {{totalCharacters}} caractères ({{percentRead}}%) de ce fichier d'une seule ligne. Ceci est une lecture partielle - le contenu restant ne peut pas être accédé en raison de limitations de contexte.",
+ "partialReadMultiLine": "Lu {{charactersRead}} sur {{totalCharacters}} caractères ({{percentRead}}%), jusqu'à la ligne {{lastLineRead}} sur {{totalLines}}. Pour lire des sections spécifiques de ce fichier, utilise le format suivant :\n\n\n \n {{path}}\n start-end\n \n\n\n\nPar exemple, pour lire les lignes {{nextLineStart}}-{{suggestedLineEnd}} :\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo semble être bloqué dans une boucle, tentant la même action ({{toolName}}) de façon répétée. Cela pourrait indiquer un problème avec sa stratégie actuelle. Envisage de reformuler la tâche, de fournir des instructions plus spécifiques ou de le guider vers une approche différente.",
"codebaseSearch": {
diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json
index e85c8d70b2..793e92ce62 100644
--- a/src/i18n/locales/hi/tools.json
+++ b/src/i18n/locales/hi/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (केवल परिभाषाएँ)",
"maxLines": " (अधिकतम {{max}} पंक्तियाँ)",
"showingOnlyLines": "कुल {{total}} पंक्तियों में से केवल {{shown}} दिखा रहे हैं। यदि आपको अधिक पंक्तियाँ पढ़नी हैं तो line_range का उपयोग करें",
- "contextLimitInstructions": "इस फ़ाइल के विशिष्ट भागों को पढ़ने के लिए, निम्नलिखित प्रारूप का उपयोग करें:\n\n\n \n {{path}}\n शुरुआत-अंत\n \n\n\n\nउदाहरण के लिए, पंक्ति 2001-3000 पढ़ने के लिए:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "छवि फ़ाइल बहुत बड़ी है ({{size}} MB)। अधिकतम अनुमतित आकार {{max}} MB है।",
- "imageWithSize": "छवि फ़ाइल ({{size}} KB)"
+ "imageWithSize": "छवि फ़ाइल ({{size}} KB)",
+ "partialReadSingleLine": "इस एकल-पंक्ति फ़ाइल से {{charactersRead}} में से {{totalCharacters}} वर्ण ({{percentRead}}%) पढ़े गए। यह एक आंशिक पठन है - शेष सामग्री संदर्भ सीमाओं के कारण पहुंच योग्य नहीं है।",
+ "partialReadMultiLine": "{{charactersRead}} में से {{totalCharacters}} वर्ण ({{percentRead}}%) पढ़े गए, {{totalLines}} में से पंक्ति {{lastLineRead}} तक। इस फ़ाइल के विशिष्ट अनुभागों को पढ़ने के लिए, निम्नलिखित प्रारूप का उपयोग करें:\n\n\n \n {{path}}\n start-end\n \n\n\n\nउदाहरण के लिए, पंक्तियां {{nextLineStart}}-{{suggestedLineEnd}} पढ़ने के लिए:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo एक लूप में फंसा हुआ लगता है, बार-बार एक ही क्रिया ({{toolName}}) को दोहरा रहा है। यह उसकी वर्तमान रणनीति में किसी समस्या का संकेत हो सकता है। कार्य को पुनः परिभाषित करने, अधिक विशिष्ट निर्देश देने, या उसे एक अलग दृष्टिकोण की ओर मार्गदर्शित करने पर विचार करें।",
"codebaseSearch": {
diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json
index 33902b2044..fc55cda4ec 100644
--- a/src/i18n/locales/id/tools.json
+++ b/src/i18n/locales/id/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (hanya definisi)",
"maxLines": " (maks {{max}} baris)",
"showingOnlyLines": "Menampilkan hanya {{shown}} dari {{total}} total baris. Gunakan line_range jika kamu perlu membaca lebih banyak baris",
- "contextLimitInstructions": "Untuk membaca bagian tertentu dari file ini, gunakan format berikut:\n\n\n \n {{path}}\n awal-akhir\n \n\n\n\nContohnya, untuk membaca baris 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "File gambar terlalu besar ({{size}} MB). Ukuran maksimum yang diizinkan adalah {{max}} MB.",
- "imageWithSize": "File gambar ({{size}} KB)"
+ "imageWithSize": "File gambar ({{size}} KB)",
+ "partialReadSingleLine": "Membaca {{charactersRead}} dari {{totalCharacters}} karakter ({{percentRead}}%) dari file satu baris ini. Ini adalah pembacaan parsial - konten yang tersisa tidak dapat diakses karena keterbatasan konteks.",
+ "partialReadMultiLine": "Membaca {{charactersRead}} dari {{totalCharacters}} karakter ({{percentRead}}%), hingga baris {{lastLineRead}} dari {{totalLines}}. Untuk membaca bagian tertentu dari file ini, gunakan format berikut:\n\n\n \n {{path}}\n start-end\n \n\n\n\nContoh, untuk membaca baris {{nextLineStart}}-{{suggestedLineEnd}}:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo tampaknya terjebak dalam loop, mencoba aksi yang sama ({{toolName}}) berulang kali. Ini mungkin menunjukkan masalah dengan strategi saat ini. Pertimbangkan untuk mengubah frasa tugas, memberikan instruksi yang lebih spesifik, atau mengarahkannya ke pendekatan yang berbeda.",
"codebaseSearch": {
diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json
index 468e3cff66..2f0b635594 100644
--- a/src/i18n/locales/it/tools.json
+++ b/src/i18n/locales/it/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (solo definizioni)",
"maxLines": " (max {{max}} righe)",
"showingOnlyLines": "Mostrando solo {{shown}} di {{total}} righe totali. Usa line_range se hai bisogno di leggere più righe",
- "contextLimitInstructions": "Per leggere sezioni specifiche di questo file, usa il seguente formato:\n\n\n \n {{path}}\n inizio-fine\n \n\n\n\nAd esempio, per leggere le righe 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "Il file immagine è troppo grande ({{size}} MB). La dimensione massima consentita è {{max}} MB.",
- "imageWithSize": "File immagine ({{size}} KB)"
+ "imageWithSize": "File immagine ({{size}} KB)",
+ "partialReadSingleLine": "Letti {{charactersRead}} di {{totalCharacters}} caratteri ({{percentRead}}%) da questo file a riga singola. Questa è una lettura parziale - il contenuto rimanente non può essere accessibile a causa di limitazioni di contesto.",
+ "partialReadMultiLine": "Letti {{charactersRead}} di {{totalCharacters}} caratteri ({{percentRead}}%), fino alla riga {{lastLineRead}} di {{totalLines}}. Per leggere sezioni specifiche di questo file, usa il seguente formato:\n\n\n \n {{path}}\n start-end\n \n\n\n\nAd esempio, per leggere le righe {{nextLineStart}}-{{suggestedLineEnd}}:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo sembra essere bloccato in un ciclo, tentando ripetutamente la stessa azione ({{toolName}}). Questo potrebbe indicare un problema con la sua strategia attuale. Considera di riformulare l'attività, fornire istruzioni più specifiche o guidarlo verso un approccio diverso.",
"codebaseSearch": {
diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json
index 4f56582cec..316ea96e9e 100644
--- a/src/i18n/locales/ja/tools.json
+++ b/src/i18n/locales/ja/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (定義のみ)",
"maxLines": " (最大{{max}}行)",
"showingOnlyLines": "全{{total}}行中{{shown}}行のみ表示しています。より多くの行を読む必要がある場合はline_rangeを使用してください",
- "contextLimitInstructions": "このファイルの特定のセクションを読むには、以下の形式を使用してください:\n\n\n \n {{path}}\n 開始-終了\n \n\n\n\n例えば、2001-3000行目を読むには:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "画像ファイルが大きすぎます({{size}} MB)。最大許可サイズは {{max}} MB です。",
- "imageWithSize": "画像ファイル({{size}} KB)"
+ "imageWithSize": "画像ファイル({{size}} KB)",
+ "partialReadSingleLine": "この単一行ファイルから{{charactersRead}}文字中{{totalCharacters}}文字({{percentRead}}%)を読み取りました。これは部分的な読み取りです - コンテキストの制限により、残りのコンテンツにはアクセスできません。",
+ "partialReadMultiLine": "{{charactersRead}}文字中{{totalCharacters}}文字({{percentRead}}%)を読み取りました。{{totalLines}}行中{{lastLineRead}}行目まで。このファイルの特定のセクションを読み取るには、次の形式を使用してください:\n\n\n \n {{path}}\n start-end\n \n\n\n\n例えば、{{nextLineStart}}-{{suggestedLineEnd}}行を読み取るには:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Rooが同じ操作({{toolName}})を繰り返し試みるループに陥っているようです。これは現在の方法に問題がある可能性を示しています。タスクの言い換え、より具体的な指示の提供、または別のアプローチへの誘導を検討してください。",
"codebaseSearch": {
diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json
index 4a0ae39174..3c3fd549f3 100644
--- a/src/i18n/locales/ko/tools.json
+++ b/src/i18n/locales/ko/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (정의만)",
"maxLines": " (최대 {{max}}행)",
"showingOnlyLines": "전체 {{total}}행 중 {{shown}}행만 표시하고 있습니다. 더 많은 행을 읽으려면 line_range를 사용하세요",
- "contextLimitInstructions": "이 파일의 특정 섹션을 읽으려면 다음 형식을 사용하세요:\n\n\n \n {{path}}\n 시작-끝\n \n\n\n\n예를 들어, 2001-3000행을 읽으려면:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "이미지 파일이 너무 큽니다 ({{size}} MB). 최대 허용 크기는 {{max}} MB입니다.",
- "imageWithSize": "이미지 파일 ({{size}} KB)"
+ "imageWithSize": "이미지 파일 ({{size}} KB)",
+ "partialReadSingleLine": "이 단일 행 파일에서 {{totalCharacters}}자 중 {{charactersRead}}자 ({{percentRead}}%)를 읽었습니다. 이는 부분 읽기입니다 - 컨텍스트 제한으로 인해 나머지 내용에 액세스할 수 없습니다.",
+ "partialReadMultiLine": "{{totalCharacters}}자 중 {{charactersRead}}자 ({{percentRead}}%)를 읽었습니다. {{totalLines}}행 중 {{lastLineRead}}행까지입니다. 이 파일의 특정 섹션을 읽으려면 다음 형식을 사용하세요:\n\n\n \n {{path}}\n start-end\n \n\n\n\n예를 들어, {{nextLineStart}}-{{suggestedLineEnd}}행을 읽으려면:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo가 같은 동작({{toolName}})을 반복적으로 시도하면서 루프에 갇힌 것 같습니다. 이는 현재 전략에 문제가 있을 수 있음을 나타냅니다. 작업을 다시 표현하거나, 더 구체적인 지침을 제공하거나, 다른 접근 방식으로 안내해 보세요.",
"codebaseSearch": {
diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json
index 5a46f7cdea..0772961c4f 100644
--- a/src/i18n/locales/nl/tools.json
+++ b/src/i18n/locales/nl/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (alleen definities)",
"maxLines": " (max {{max}} regels)",
"showingOnlyLines": "Toont alleen {{shown}} van {{total}} totale regels. Gebruik line_range als je meer regels wilt lezen",
- "contextLimitInstructions": "Om specifieke secties van dit bestand te lezen, gebruik het volgende formaat:\n\n\n \n {{path}}\n start-eind\n \n\n\n\nBijvoorbeeld, om regels 2001-3000 te lezen:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "Afbeeldingsbestand is te groot ({{size}} MB). De maximaal toegestane grootte is {{max}} MB.",
- "imageWithSize": "Afbeeldingsbestand ({{size}} KB)"
+ "imageWithSize": "Afbeeldingsbestand ({{size}} KB)",
+ "partialReadSingleLine": "{{charactersRead}} van {{totalCharacters}} tekens ({{percentRead}}%) gelezen van dit bestand met één regel. Dit is een gedeeltelijke lezing - de resterende inhoud is niet toegankelijk vanwege contextbeperkingen.",
+ "partialReadMultiLine": "{{charactersRead}} van {{totalCharacters}} tekens ({{percentRead}}%) gelezen, tot regel {{lastLineRead}} van {{totalLines}}. Om specifieke secties van dit bestand te lezen, gebruik je het volgende formaat:\n\n\n \n {{path}}\n start-end\n \n\n\n\nBijvoorbeeld, om regels {{nextLineStart}}-{{suggestedLineEnd}} te lezen:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo lijkt vast te zitten in een lus, waarbij hij herhaaldelijk dezelfde actie ({{toolName}}) probeert. Dit kan duiden op een probleem met de huidige strategie. Overweeg de taak te herformuleren, specifiekere instructies te geven of Roo naar een andere aanpak te leiden.",
"codebaseSearch": {
diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json
index e872f7b215..9258dfe2f3 100644
--- a/src/i18n/locales/pl/tools.json
+++ b/src/i18n/locales/pl/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (tylko definicje)",
"maxLines": " (maks. {{max}} linii)",
"showingOnlyLines": "Pokazuję tylko {{shown}} z {{total}} wszystkich linii. Użyj line_range jeśli potrzebujesz przeczytać więcej linii",
- "contextLimitInstructions": "Aby przeczytać określone sekcje tego pliku, użyj następującego formatu:\n\n\n \n {{path}}\n początek-koniec\n \n\n\n\nNa przykład, aby przeczytać linie 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "Plik obrazu jest zbyt duży ({{size}} MB). Maksymalny dozwolony rozmiar to {{max}} MB.",
- "imageWithSize": "Plik obrazu ({{size}} KB)"
+ "imageWithSize": "Plik obrazu ({{size}} KB)",
+ "partialReadSingleLine": "Przeczytano {{charactersRead}} z {{totalCharacters}} znaków ({{percentRead}}%) z tego jednoliniowego pliku. To jest częściowy odczyt - pozostała zawartość nie może być dostępna z powodu ograniczeń kontekstu.",
+ "partialReadMultiLine": "Przeczytano {{charactersRead}} z {{totalCharacters}} znaków ({{percentRead}}%), do linii {{lastLineRead}} z {{totalLines}}. Aby przeczytać określone sekcje tego pliku, użyj następującego formatu:\n\n\n \n {{path}}\n start-end\n \n\n\n\nNa przykład, aby przeczytać linie {{nextLineStart}}-{{suggestedLineEnd}}:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Wygląda na to, że Roo utknął w pętli, wielokrotnie próbując wykonać tę samą akcję ({{toolName}}). Może to wskazywać na problem z jego obecną strategią. Rozważ przeformułowanie zadania, podanie bardziej szczegółowych instrukcji lub nakierowanie go na inne podejście.",
"codebaseSearch": {
diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json
index b47112aa84..5bcf94f559 100644
--- a/src/i18n/locales/pt-BR/tools.json
+++ b/src/i18n/locales/pt-BR/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (apenas definições)",
"maxLines": " (máx. {{max}} linhas)",
"showingOnlyLines": "Mostrando apenas {{shown}} de {{total}} linhas totais. Use line_range se precisar ler mais linhas",
- "contextLimitInstructions": "Para ler seções específicas deste arquivo, use o seguinte formato:\n\n\n \n {{path}}\n início-fim\n \n\n\n\nPor exemplo, para ler as linhas 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "Arquivo de imagem é muito grande ({{size}} MB). O tamanho máximo permitido é {{max}} MB.",
- "imageWithSize": "Arquivo de imagem ({{size}} KB)"
+ "imageWithSize": "Arquivo de imagem ({{size}} KB)",
+ "partialReadSingleLine": "Lidos {{charactersRead}} de {{totalCharacters}} caracteres ({{percentRead}}%) deste arquivo de linha única. Esta é uma leitura parcial - o conteúdo restante não pode ser acessado devido a limitações de contexto.",
+ "partialReadMultiLine": "Lidos {{charactersRead}} de {{totalCharacters}} caracteres ({{percentRead}}%), até a linha {{lastLineRead}} de {{totalLines}}. Para ler seções específicas deste arquivo, use o seguinte formato:\n\n\n \n {{path}}\n start-end\n \n\n\n\nPor exemplo, para ler as linhas {{nextLineStart}}-{{suggestedLineEnd}}:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo parece estar preso em um loop, tentando a mesma ação ({{toolName}}) repetidamente. Isso pode indicar um problema com sua estratégia atual. Considere reformular a tarefa, fornecer instruções mais específicas ou guiá-lo para uma abordagem diferente.",
"codebaseSearch": {
diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json
index 28ddb7d942..fe9e3996ed 100644
--- a/src/i18n/locales/ru/tools.json
+++ b/src/i18n/locales/ru/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (только определения)",
"maxLines": " (макс. {{max}} строк)",
"showingOnlyLines": "Показано только {{shown}} из {{total}} общих строк. Используй line_range если нужно прочитать больше строк",
- "contextLimitInstructions": "Чтобы прочитать определенные разделы этого файла, используй следующий формат:\n\n\n \n {{path}}\n начало-конец\n \n\n\n\nНапример, чтобы прочитать строки 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "Файл изображения слишком большой ({{size}} МБ). Максимально допустимый размер {{max}} МБ.",
- "imageWithSize": "Файл изображения ({{size}} КБ)"
+ "imageWithSize": "Файл изображения ({{size}} КБ)",
+ "partialReadSingleLine": "Прочитано {{charactersRead}} из {{totalCharacters}} символов ({{percentRead}}%) из этого однострочного файла. Это частичное чтение - оставшееся содержимое недоступно из-за ограничений контекста.",
+ "partialReadMultiLine": "Прочитано {{charactersRead}} из {{totalCharacters}} символов ({{percentRead}}%), до строки {{lastLineRead}} из {{totalLines}}. Чтобы прочитать определенные разделы этого файла, используйте следующий формат:\n\n\n \n {{path}}\n start-end\n \n\n\n\nНапример, чтобы прочитать строки {{nextLineStart}}-{{suggestedLineEnd}}:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Похоже, что Roo застрял в цикле, многократно пытаясь выполнить одно и то же действие ({{toolName}}). Это может указывать на проблему с его текущей стратегией. Попробуйте переформулировать задачу, предоставить более конкретные инструкции или направить его к другому подходу.",
"codebaseSearch": {
diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json
index c576692e48..985af11823 100644
--- a/src/i18n/locales/tr/tools.json
+++ b/src/i18n/locales/tr/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (sadece tanımlar)",
"maxLines": " (maks. {{max}} satır)",
"showingOnlyLines": "Toplam {{total}} satırdan sadece {{shown}} tanesi gösteriliyor. Daha fazla satır okumak için line_range kullan",
- "contextLimitInstructions": "Bu dosyanın belirli bölümlerini okumak için aşağıdaki formatı kullan:\n\n\n \n {{path}}\n başlangıç-bitiş\n \n\n\n\nÖrneğin, 2001-3000 satırlarını okumak için:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "Görüntü dosyası çok büyük ({{size}} MB). İzin verilen maksimum boyut {{max}} MB.",
- "imageWithSize": "Görüntü dosyası ({{size}} KB)"
+ "imageWithSize": "Görüntü dosyası ({{size}} KB)",
+ "partialReadSingleLine": "Bu tek satırlık dosyadan {{totalCharacters}} karakterden {{charactersRead}} karakter ({{percentRead}}%) okundu. Bu kısmi bir okuma - kalan içeriğe bağlam sınırlamaları nedeniyle erişilemiyor.",
+ "partialReadMultiLine": "{{totalCharacters}} karakterden {{charactersRead}} karakter ({{percentRead}}%) okundu, {{totalLines}} satırdan {{lastLineRead}}. satıra kadar. Bu dosyanın belirli bölümlerini okumak için aşağıdaki formatı kullan:\n\n\n \n {{path}}\n start-end\n \n\n\n\nÖrneğin, {{nextLineStart}}-{{suggestedLineEnd}} satırlarını okumak için:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo bir döngüye takılmış gibi görünüyor, aynı eylemi ({{toolName}}) tekrar tekrar deniyor. Bu, mevcut stratejisinde bir sorun olduğunu gösterebilir. Görevi yeniden ifade etmeyi, daha spesifik talimatlar vermeyi veya onu farklı bir yaklaşıma yönlendirmeyi düşünün.",
"codebaseSearch": {
diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json
index eeca7a5105..574d2b346e 100644
--- a/src/i18n/locales/vi/tools.json
+++ b/src/i18n/locales/vi/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (chỉ định nghĩa)",
"maxLines": " (tối đa {{max}} dòng)",
"showingOnlyLines": "Chỉ hiển thị {{shown}} trong tổng số {{total}} dòng. Sử dụng line_range nếu bạn cần đọc thêm dòng",
- "contextLimitInstructions": "Để đọc các phần cụ thể của tệp này, hãy sử dụng định dạng sau:\n\n\n \n {{path}}\n bắt đầu-kết thúc\n \n\n\n\nVí dụ, để đọc dòng 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "Tệp hình ảnh quá lớn ({{size}} MB). Kích thước tối đa cho phép là {{max}} MB.",
- "imageWithSize": "Tệp hình ảnh ({{size}} KB)"
+ "imageWithSize": "Tệp hình ảnh ({{size}} KB)",
+ "partialReadSingleLine": "Đã đọc {{charactersRead}} trong số {{totalCharacters}} ký tự ({{percentRead}}%) từ tệp một dòng này. Đây là việc đọc một phần - nội dung còn lại không thể truy cập được do giới hạn ngữ cảnh.",
+ "partialReadMultiLine": "Đã đọc {{charactersRead}} trong số {{totalCharacters}} ký tự ({{percentRead}}%), đến dòng {{lastLineRead}} trong tổng số {{totalLines}} dòng. Để đọc các phần cụ thể của tệp này, hãy sử dụng định dạng sau:\n\n\n \n {{path}}\n start-end\n \n\n\n\nVí dụ, để đọc các dòng {{nextLineStart}}-{{suggestedLineEnd}}:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo dường như đang bị mắc kẹt trong một vòng lặp, liên tục cố gắng thực hiện cùng một hành động ({{toolName}}). Điều này có thể cho thấy vấn đề với chiến lược hiện tại. Hãy cân nhắc việc diễn đạt lại nhiệm vụ, cung cấp hướng dẫn cụ thể hơn, hoặc hướng Roo theo một cách tiếp cận khác.",
"codebaseSearch": {
diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json
index a8e0adb81a..d8961e7268 100644
--- a/src/i18n/locales/zh-CN/tools.json
+++ b/src/i18n/locales/zh-CN/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (仅定义)",
"maxLines": " (最多 {{max}} 行)",
"showingOnlyLines": "仅显示 {{shown}} 行,共 {{total}} 行。如需阅读更多行请使用 line_range",
- "contextLimitInstructions": "要阅读此文件的特定部分,请使用以下格式:\n\n\n \n {{path}}\n 开始-结束\n \n\n\n\n例如,要阅读第 2001-3000 行:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "图片文件过大 ({{size}} MB)。允许的最大大小为 {{max}} MB。",
- "imageWithSize": "图片文件 ({{size}} KB)"
+ "imageWithSize": "图片文件 ({{size}} KB)",
+ "partialReadSingleLine": "已读取此单行文件中 {{totalCharacters}} 个字符中的 {{charactersRead}} 个字符 ({{percentRead}}%)。这是部分读取 - 由于上下文限制,无法访问剩余内容。",
+ "partialReadMultiLine": "已读取 {{totalCharacters}} 个字符中的 {{charactersRead}} 个字符 ({{percentRead}}%),读取到第 {{lastLineRead}} 行,共 {{totalLines}} 行。要读取此文件的特定部分,请使用以下格式:\n\n\n \n {{path}}\n start-end\n \n\n\n\n例如,要读取第 {{nextLineStart}}-{{suggestedLineEnd}} 行:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo 似乎陷入循环,反复尝试同一操作 ({{toolName}})。这可能表明当前策略存在问题。请考虑重新描述任务、提供更具体的指示或引导其尝试不同的方法。",
"codebaseSearch": {
diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json
index ab4bd92209..4a27b04090 100644
--- a/src/i18n/locales/zh-TW/tools.json
+++ b/src/i18n/locales/zh-TW/tools.json
@@ -4,9 +4,10 @@
"definitionsOnly": " (僅定義)",
"maxLines": " (最多 {{max}} 行)",
"showingOnlyLines": "僅顯示 {{shown}} 行,共 {{total}} 行。如需閱讀更多行請使用 line_range",
- "contextLimitInstructions": "要閱讀此檔案的特定部分,請使用以下格式:\n\n\n \n {{path}}\n 開始-結束\n \n\n\n\n例如,要閱讀第 2001-3000 行:\n\n\n \n {{path}}\n 2001-3000\n \n\n",
"imageTooLarge": "圖片檔案過大 ({{size}} MB)。允許的最大大小為 {{max}} MB。",
- "imageWithSize": "圖片檔案 ({{size}} KB)"
+ "imageWithSize": "圖片檔案 ({{size}} KB)",
+ "partialReadSingleLine": "已讀取此單行檔案中 {{totalCharacters}} 個字元中的 {{charactersRead}} 個字元 ({{percentRead}}%)。這是部分讀取 - 由於內容限制,無法存取剩餘內容。",
+ "partialReadMultiLine": "已讀取 {{totalCharacters}} 個字元中的 {{charactersRead}} 個字元 ({{percentRead}}%),讀取到第 {{lastLineRead}} 行,共 {{totalLines}} 行。要讀取此檔案的特定部分,請使用以下格式:\n\n\n \n {{path}}\n start-end\n \n\n\n\n例如,要讀取第 {{nextLineStart}}-{{suggestedLineEnd}} 行:\n\n\n \n {{path}}\n {{nextLineStart}}-{{suggestedLineEnd}}\n \n\n"
},
"toolRepetitionLimitReached": "Roo 似乎陷入循環,反覆嘗試同一操作 ({{toolName}})。這可能表明目前策略存在問題。請考慮重新描述工作、提供更具體的指示或引導其嘗試不同的方法。",
"codebaseSearch": {
diff --git a/src/integrations/misc/__tests__/read-partial-content.spec.ts b/src/integrations/misc/__tests__/read-partial-content.spec.ts
index 7f46c753cb..eea72ce0f2 100644
--- a/src/integrations/misc/__tests__/read-partial-content.spec.ts
+++ b/src/integrations/misc/__tests__/read-partial-content.spec.ts
@@ -1,10 +1,10 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
-import { readPartialSingleLineContent } from "../read-partial-content"
+import { readPartialSingleLineContent, readPartialContent } from "../read-partial-content"
import * as fs from "fs"
import * as path from "path"
import * as os from "os"
-describe("readPartialSingleLineContent", () => {
+describe("read-partial-content", () => {
let tempDir: string
let testFiles: string[] = []
@@ -37,218 +37,393 @@ describe("readPartialSingleLineContent", () => {
return filePath
}
- describe("Basic functionality", () => {
- it("should read partial content from a small file", async () => {
- const content = "Hello, world! This is a test file."
- const filePath = await createTestFile("small.txt", content)
+ describe("readPartialContent", () => {
+ describe("Basic functionality", () => {
+ it("should read partial content with line tracking", async () => {
+ const content = "Line 1\nLine 2\nLine 3\nLine 4"
+ const filePath = await createTestFile("multiline.txt", content)
- const result = await readPartialSingleLineContent(filePath, 10)
+ const result = await readPartialContent(filePath, 15)
- expect(result).toBe("Hello, wor")
+ expect(result.content).toBe("Line 1\nLine 2\nL")
+ expect(result.charactersRead).toBe(15)
+ expect(result.totalCharacters).toBe(content.length)
+ expect(result.linesRead).toBe(3) // Counting starts at 1, and we read into line 3
+ expect(result.totalLines).toBe(4)
+ expect(result.lastLineRead).toBe(3)
+ })
+
+ it("should handle single-line files", async () => {
+ const content = "This is a single line file with no newlines"
+ const filePath = await createTestFile("single-line.txt", content)
+
+ const result = await readPartialContent(filePath, 20)
+
+ expect(result.content).toBe("This is a single lin")
+ expect(result.charactersRead).toBe(20)
+ expect(result.linesRead).toBe(1)
+ expect(result.totalLines).toBe(1)
+ expect(result.lastLineRead).toBe(1)
+ })
+
+ it("should read entire file when maxChars exceeds file size", async () => {
+ const content = "Small\nFile\nContent"
+ const filePath = await createTestFile("small.txt", content)
+
+ const result = await readPartialContent(filePath, 1000)
+
+ expect(result.content).toBe(content)
+ expect(result.charactersRead).toBe(content.length)
+ expect(result.totalCharacters).toBe(content.length)
+ expect(result.linesRead).toBe(3)
+ expect(result.totalLines).toBe(3)
+ expect(result.lastLineRead).toBe(3)
+ })
+
+ it("should handle empty files", async () => {
+ const filePath = await createTestFile("empty.txt", "")
+
+ const result = await readPartialContent(filePath, 10)
+
+ expect(result.content).toBe("")
+ expect(result.charactersRead).toBe(0)
+ expect(result.totalCharacters).toBe(0)
+ expect(result.linesRead).toBe(0)
+ expect(result.totalLines).toBe(0)
+ expect(result.lastLineRead).toBe(0)
+ })
+
+ it("should handle maxChars of 0", async () => {
+ const content = "This content should not be read"
+ const filePath = await createTestFile("zero-chars.txt", content)
+
+ const result = await readPartialContent(filePath, 0)
+
+ expect(result.content).toBe("")
+ expect(result.charactersRead).toBe(0)
+ expect(result.linesRead).toBe(0)
+ expect(result.lastLineRead).toBe(0)
+ })
})
- it("should read entire content when maxChars exceeds file size", async () => {
- const content = "Short file"
- const filePath = await createTestFile("short.txt", content)
+ describe("Line counting accuracy", () => {
+ it("should count lines correctly when stopping mid-line", async () => {
+ const content = "Line 1\nLine 2 is longer\nLine 3"
+ const filePath = await createTestFile("mid-line.txt", content)
- const result = await readPartialSingleLineContent(filePath, 100)
+ const result = await readPartialContent(filePath, 10)
- expect(result).toBe(content)
+ expect(result.content).toBe("Line 1\nLin")
+ expect(result.linesRead).toBe(2) // We're in line 2
+ expect(result.lastLineRead).toBe(2)
+ })
+
+ it("should count lines correctly when stopping at newline", async () => {
+ const content = "Line 1\nLine 2\nLine 3"
+ const filePath = await createTestFile("at-newline.txt", content)
+
+ const result = await readPartialContent(filePath, 7) // Exactly at the first newline
+
+ expect(result.content).toBe("Line 1\n")
+ expect(result.linesRead).toBe(2) // We've entered line 2
+ expect(result.lastLineRead).toBe(2)
+ })
+
+ it("should handle files with empty lines", async () => {
+ const content = "Line 1\n\nLine 3\n\n\nLine 6"
+ const filePath = await createTestFile("empty-lines.txt", content)
+
+ const result = await readPartialContent(filePath, 15)
+
+ expect(result.content).toBe("Line 1\n\nLine 3\n")
+ expect(result.linesRead).toBe(4) // We've entered line 4
+ expect(result.totalLines).toBe(6)
+ })
+
+ it("should handle files ending with newline", async () => {
+ const content = "Line 1\nLine 2\n"
+ const filePath = await createTestFile("ending-newline.txt", content)
+
+ const result = await readPartialContent(filePath, 100)
+
+ expect(result.content).toBe(content)
+ expect(result.linesRead).toBe(3) // The empty line after the last newline
+ expect(result.totalLines).toBe(2) // countFileLines counts actual lines, not the trailing empty line
+ })
})
- it("should handle empty files", async () => {
- const filePath = await createTestFile("empty.txt", "")
+ describe("Large file handling", () => {
+ it("should handle large files with many lines", async () => {
+ const lines = Array.from({ length: 1000 }, (_, i) => `Line ${i + 1}`).join("\n")
+ const filePath = await createTestFile("many-lines.txt", lines)
- const result = await readPartialSingleLineContent(filePath, 10)
+ const result = await readPartialContent(filePath, 100)
- expect(result).toBe("")
+ expect(result.charactersRead).toBe(100)
+ expect(result.totalLines).toBe(1000)
+ expect(result.linesRead).toBeGreaterThan(1)
+ expect(result.linesRead).toBeLessThan(50) // Should not have read too many lines
+ })
+
+ it("should handle very long single lines", async () => {
+ const content = "x".repeat(100000) // 100KB single line
+ const filePath = await createTestFile("long-single-line.txt", content)
+
+ const result = await readPartialContent(filePath, 1000)
+
+ expect(result.content).toBe("x".repeat(1000))
+ expect(result.linesRead).toBe(1)
+ expect(result.totalLines).toBe(1)
+ expect(result.lastLineRead).toBe(1)
+ })
})
- it("should handle maxChars of 0", async () => {
- const content = "This content should not be read"
- const filePath = await createTestFile("zero-chars.txt", content)
+ describe("Unicode and special characters", () => {
+ it("should handle Unicode characters with line tracking", async () => {
+ const content = "Hello 世界!\n🌍 Émojis\nñoñó chars"
+ const filePath = await createTestFile("unicode-lines.txt", content)
- const result = await readPartialSingleLineContent(filePath, 0)
+ const result = await readPartialContent(filePath, 20)
- expect(result).toBe("")
+ expect(result.linesRead).toBeGreaterThanOrEqual(2)
+ expect(result.totalLines).toBe(3)
+ })
+ })
+
+ describe("Error handling", () => {
+ it("should reject when file does not exist", async () => {
+ const nonExistentPath = path.join(tempDir, "does-not-exist.txt")
+
+ await expect(readPartialContent(nonExistentPath, 10)).rejects.toThrow()
+ })
+
+ it("should handle negative maxChars gracefully", async () => {
+ const content = "Test content"
+ const filePath = await createTestFile("negative-max.txt", content)
+
+ const result = await readPartialContent(filePath, -5)
+
+ expect(result.content).toBe("")
+ expect(result.charactersRead).toBe(0)
+ expect(result.linesRead).toBe(0)
+ })
})
})
- describe("Large file handling", () => {
- it("should handle large files efficiently", async () => {
- // Create a large file (1MB of repeated text)
- const chunk = "This is a repeated chunk of text that will be used to create a large file. "
- const largeContent = chunk.repeat(Math.ceil((1024 * 1024) / chunk.length))
- const filePath = await createTestFile("large.txt", largeContent)
+ describe("readPartialSingleLineContent (legacy)", () => {
+ describe("Basic functionality", () => {
+ it("should read partial content from a small file", async () => {
+ const content = "Hello, world! This is a test file."
+ const filePath = await createTestFile("small.txt", content)
- const result = await readPartialSingleLineContent(filePath, 100)
+ const result = await readPartialSingleLineContent(filePath, 10)
- expect(result).toBe(largeContent.substring(0, 100))
- expect(result.length).toBe(100)
+ expect(result).toBe("Hello, wor")
+ })
+
+ it("should read entire content when maxChars exceeds file size", async () => {
+ const content = "Short file"
+ const filePath = await createTestFile("short.txt", content)
+
+ const result = await readPartialSingleLineContent(filePath, 100)
+
+ expect(result).toBe(content)
+ })
+
+ it("should handle empty files", async () => {
+ const filePath = await createTestFile("empty.txt", "")
+
+ const result = await readPartialSingleLineContent(filePath, 10)
+
+ expect(result).toBe("")
+ })
+
+ it("should handle maxChars of 0", async () => {
+ const content = "This content should not be read"
+ const filePath = await createTestFile("zero-chars.txt", content)
+
+ const result = await readPartialSingleLineContent(filePath, 0)
+
+ expect(result).toBe("")
+ })
})
- it("should handle very large maxChars values", async () => {
- const content = "Small content for large maxChars test"
- const filePath = await createTestFile("small-for-large-max.txt", content)
+ describe("Large file handling", () => {
+ it("should handle large files efficiently", async () => {
+ // Create a large file (1MB of repeated text)
+ const chunk = "This is a repeated chunk of text that will be used to create a large file. "
+ const largeContent = chunk.repeat(Math.ceil((1024 * 1024) / chunk.length))
+ const filePath = await createTestFile("large.txt", largeContent)
- const result = await readPartialSingleLineContent(filePath, 1000000)
+ const result = await readPartialSingleLineContent(filePath, 100)
- expect(result).toBe(content)
- })
- })
+ expect(result).toBe(largeContent.substring(0, 100))
+ expect(result.length).toBe(100)
+ })
- describe("Unicode and special characters", () => {
- it("should handle Unicode characters correctly", async () => {
- const content = "Hello 世界! 🌍 Émojis and ñoñó characters"
- const filePath = await createTestFile("unicode.txt", content)
+ it("should handle very large maxChars values", async () => {
+ const content = "Small content for large maxChars test"
+ const filePath = await createTestFile("small-for-large-max.txt", content)
- const result = await readPartialSingleLineContent(filePath, 15)
+ const result = await readPartialSingleLineContent(filePath, 1000000)
- // Should handle Unicode characters properly
- expect(result.length).toBeLessThanOrEqual(15)
- expect(result).toBe(content.substring(0, result.length))
+ expect(result).toBe(content)
+ })
})
- it("should handle newlines in content", async () => {
- const content = "Line 1\nLine 2\nLine 3"
- const filePath = await createTestFile("multiline.txt", content)
+ describe("Unicode and special characters", () => {
+ it("should handle Unicode characters correctly", async () => {
+ const content = "Hello 世界! 🌍 Émojis and ñoñó characters"
+ const filePath = await createTestFile("unicode.txt", content)
- const result = await readPartialSingleLineContent(filePath, 10)
+ const result = await readPartialSingleLineContent(filePath, 15)
- expect(result).toBe("Line 1\nLin")
+ // Should handle Unicode characters properly
+ expect(result.length).toBeLessThanOrEqual(15)
+ expect(result).toBe(content.substring(0, result.length))
+ })
+
+ it("should handle newlines in content", async () => {
+ const content = "Line 1\nLine 2\nLine 3"
+ const filePath = await createTestFile("multiline.txt", content)
+
+ const result = await readPartialSingleLineContent(filePath, 10)
+
+ expect(result).toBe("Line 1\nLin")
+ })
+
+ it("should handle special characters and symbols", async () => {
+ const content = "Special chars: !@#$%^&*()_+-=[]{}|;':\",./<>?"
+ const filePath = await createTestFile("special.txt", content)
+
+ const result = await readPartialSingleLineContent(filePath, 20)
+
+ expect(result).toBe("Special chars: !@#$%")
+ })
})
- it("should handle special characters and symbols", async () => {
- const content = "Special chars: !@#$%^&*()_+-=[]{}|;':\",./<>?"
- const filePath = await createTestFile("special.txt", content)
+ describe("Edge cases", () => {
+ it("should handle exact character limit", async () => {
+ const content = "Exactly twenty chars"
+ const filePath = await createTestFile("exact.txt", content)
- const result = await readPartialSingleLineContent(filePath, 20)
+ const result = await readPartialSingleLineContent(filePath, 20)
- expect(result).toBe("Special chars: !@#$%")
- })
- })
+ expect(result).toBe(content)
+ expect(result.length).toBe(20)
+ })
- describe("Edge cases", () => {
- it("should handle exact character limit", async () => {
- const content = "Exactly twenty chars"
- const filePath = await createTestFile("exact.txt", content)
+ it("should handle maxChars = 1", async () => {
+ const content = "Single character test"
+ const filePath = await createTestFile("single-char.txt", content)
- const result = await readPartialSingleLineContent(filePath, 20)
+ const result = await readPartialSingleLineContent(filePath, 1)
- expect(result).toBe(content)
- expect(result.length).toBe(20)
+ expect(result).toBe("S")
+ })
+
+ it("should handle files with only whitespace", async () => {
+ const content = " \t\n "
+ const filePath = await createTestFile("whitespace.txt", content)
+
+ const result = await readPartialSingleLineContent(filePath, 5)
+
+ expect(result).toBe(" \t\n")
+ })
})
- it("should handle maxChars = 1", async () => {
- const content = "Single character test"
- const filePath = await createTestFile("single-char.txt", content)
+ describe("Error handling", () => {
+ it("should reject when file does not exist", async () => {
+ const nonExistentPath = path.join(tempDir, "does-not-exist.txt")
- const result = await readPartialSingleLineContent(filePath, 1)
+ await expect(readPartialSingleLineContent(nonExistentPath, 10)).rejects.toThrow()
+ })
- expect(result).toBe("S")
+ it("should reject when file path is invalid", async () => {
+ const invalidPath = "\0invalid\0path"
+
+ await expect(readPartialSingleLineContent(invalidPath, 10)).rejects.toThrow()
+ })
+
+ it("should handle negative maxChars gracefully", async () => {
+ const content = "Test content"
+ const filePath = await createTestFile("negative-max.txt", content)
+
+ const result = await readPartialSingleLineContent(filePath, -5)
+
+ expect(result).toBe("")
+ })
})
- it("should handle files with only whitespace", async () => {
- const content = " \t\n "
- const filePath = await createTestFile("whitespace.txt", content)
+ describe("Performance and memory efficiency", () => {
+ it("should not load entire large file into memory", async () => {
+ // Create a file larger than typical memory chunks
+ const largeContent = "x".repeat(5 * 1024 * 1024) // 5MB file
+ const filePath = await createTestFile("memory-test.txt", largeContent)
- const result = await readPartialSingleLineContent(filePath, 5)
+ // Read only a small portion
+ const result = await readPartialSingleLineContent(filePath, 1000)
- expect(result).toBe(" \t\n")
- })
- })
+ expect(result).toBe("x".repeat(1000))
+ expect(result.length).toBe(1000)
+ })
- describe("Error handling", () => {
- it("should reject when file does not exist", async () => {
- const nonExistentPath = path.join(tempDir, "does-not-exist.txt")
+ it("should handle multiple consecutive reads efficiently", async () => {
+ const content = "Repeated read test content that is somewhat long"
+ const filePath = await createTestFile("repeated-read.txt", content)
- await expect(readPartialSingleLineContent(nonExistentPath, 10)).rejects.toThrow()
+ // Perform multiple reads
+ const results = await Promise.all([
+ readPartialSingleLineContent(filePath, 10),
+ readPartialSingleLineContent(filePath, 20),
+ readPartialSingleLineContent(filePath, 30),
+ ])
+
+ expect(results[0]).toBe(content.substring(0, 10))
+ expect(results[1]).toBe(content.substring(0, 20))
+ expect(results[2]).toBe(content.substring(0, 30))
+ })
})
- it("should reject when file path is invalid", async () => {
- const invalidPath = "\0invalid\0path"
+ describe("Stream handling", () => {
+ it("should handle normal stream completion", async () => {
+ const content = "Stream test content"
+ const filePath = await createTestFile("stream-test.txt", content)
- await expect(readPartialSingleLineContent(invalidPath, 10)).rejects.toThrow()
+ const result = await readPartialSingleLineContent(filePath, 10)
+
+ expect(result).toBe("Stream tes")
+ })
+
+ it("should handle file access errors", async () => {
+ // Test with a directory instead of a file to trigger an error
+ await expect(readPartialSingleLineContent(tempDir, 10)).rejects.toThrow()
+ })
})
- it("should handle negative maxChars gracefully", async () => {
- const content = "Test content"
- const filePath = await createTestFile("negative-max.txt", content)
+ describe("Boundary conditions", () => {
+ it("should handle chunk boundaries correctly", async () => {
+ // Create content that will span multiple chunks
+ const chunkSize = 16 * 1024 // Default highWaterMark
+ const content = "a".repeat(chunkSize + 100)
+ const filePath = await createTestFile("chunk-boundary.txt", content)
- const result = await readPartialSingleLineContent(filePath, -5)
+ const result = await readPartialSingleLineContent(filePath, chunkSize + 50)
- expect(result).toBe("")
- })
- })
+ expect(result).toBe("a".repeat(chunkSize + 50))
+ expect(result.length).toBe(chunkSize + 50)
+ })
- describe("Performance and memory efficiency", () => {
- it("should not load entire large file into memory", async () => {
- // Create a file larger than typical memory chunks
- const largeContent = "x".repeat(5 * 1024 * 1024) // 5MB file
- const filePath = await createTestFile("memory-test.txt", largeContent)
+ it("should handle maxChars at chunk boundary", async () => {
+ const chunkSize = 16 * 1024
+ const content = "b".repeat(chunkSize * 2)
+ const filePath = await createTestFile("exact-chunk.txt", content)
- // Read only a small portion
- const result = await readPartialSingleLineContent(filePath, 1000)
+ const result = await readPartialSingleLineContent(filePath, chunkSize)
- expect(result).toBe("x".repeat(1000))
- expect(result.length).toBe(1000)
- })
-
- it("should handle multiple consecutive reads efficiently", async () => {
- const content = "Repeated read test content that is somewhat long"
- const filePath = await createTestFile("repeated-read.txt", content)
-
- // Perform multiple reads
- const results = await Promise.all([
- readPartialSingleLineContent(filePath, 10),
- readPartialSingleLineContent(filePath, 20),
- readPartialSingleLineContent(filePath, 30),
- ])
-
- expect(results[0]).toBe(content.substring(0, 10))
- expect(results[1]).toBe(content.substring(0, 20))
- expect(results[2]).toBe(content.substring(0, 30))
- })
- })
-
- describe("Stream handling", () => {
- it("should handle normal stream completion", async () => {
- const content = "Stream test content"
- const filePath = await createTestFile("stream-test.txt", content)
-
- const result = await readPartialSingleLineContent(filePath, 10)
-
- expect(result).toBe("Stream tes")
- })
-
- it("should handle file access errors", async () => {
- // Test with a directory instead of a file to trigger an error
- await expect(readPartialSingleLineContent(tempDir, 10)).rejects.toThrow()
- })
- })
-
- describe("Boundary conditions", () => {
- it("should handle chunk boundaries correctly", async () => {
- // Create content that will span multiple chunks
- const chunkSize = 16 * 1024 // Default highWaterMark
- const content = "a".repeat(chunkSize + 100)
- const filePath = await createTestFile("chunk-boundary.txt", content)
-
- const result = await readPartialSingleLineContent(filePath, chunkSize + 50)
-
- expect(result).toBe("a".repeat(chunkSize + 50))
- expect(result.length).toBe(chunkSize + 50)
- })
-
- it("should handle maxChars at chunk boundary", async () => {
- const chunkSize = 16 * 1024
- const content = "b".repeat(chunkSize * 2)
- const filePath = await createTestFile("exact-chunk.txt", content)
-
- const result = await readPartialSingleLineContent(filePath, chunkSize)
-
- expect(result).toBe("b".repeat(chunkSize))
- expect(result.length).toBe(chunkSize)
+ expect(result).toBe("b".repeat(chunkSize))
+ expect(result.length).toBe(chunkSize)
+ })
})
})
})
diff --git a/src/integrations/misc/read-partial-content.ts b/src/integrations/misc/read-partial-content.ts
index 4a818bb04a..e8db809982 100644
--- a/src/integrations/misc/read-partial-content.ts
+++ b/src/integrations/misc/read-partial-content.ts
@@ -1,21 +1,47 @@
import { createReadStream } from "fs"
+import * as fs from "fs/promises"
+import { countFileLines } from "./line-counter"
/**
- * Reads partial content from a single-line file up to a specified character limit.
+ * Result of a partial file read operation
+ */
+export interface PartialReadResult {
+ content: string
+ charactersRead: number
+ totalCharacters: number // from file stats
+ linesRead: number
+ totalLines: number // from line counter
+ lastLineRead: number // which line we stopped at
+}
+
+/**
+ * Reads partial content from a file up to a specified character limit.
+ * Works for both single-line and multi-line files, tracking line numbers.
* Uses streaming to avoid loading the entire file into memory for very large files.
*
* @param filePath - Path to the file to read
* @param maxChars - Maximum number of characters to read
- * @returns Promise resolving to the partial content as a string
+ * @returns Promise resolving to the partial read result with metadata
*/
-export function readPartialSingleLineContent(filePath: string, maxChars: number): Promise {
- return new Promise((resolve, reject) => {
- // Handle edge cases
- if (maxChars <= 0) {
- resolve("")
- return
- }
+export async function readPartialContent(filePath: string, maxChars: number): Promise {
+ // Get file stats and line count
+ const [stats, totalLines] = await Promise.all([fs.stat(filePath), countFileLines(filePath)])
+ const totalCharacters = stats.size
+
+ // Handle edge cases
+ if (maxChars <= 0 || totalCharacters === 0) {
+ return {
+ content: "",
+ charactersRead: 0,
+ totalCharacters,
+ linesRead: 0,
+ totalLines,
+ lastLineRead: 0,
+ }
+ }
+
+ return new Promise((resolve, reject) => {
// Use smaller chunks and set end position to limit reading
const stream = createReadStream(filePath, {
encoding: "utf8",
@@ -23,9 +49,12 @@ export function readPartialSingleLineContent(filePath: string, maxChars: number)
start: 0,
end: Math.max(0, Math.min(maxChars * 2, maxChars + 1024 * 1024)), // Read at most 2x maxChars or maxChars + 1MB buffer
})
+
let content = ""
let totalRead = 0
+ let currentLine = 1
let streamDestroyed = false
+ let hasContent = false
stream.on("data", (chunk: string | Buffer) => {
// Early exit if stream was already destroyed
@@ -40,27 +69,66 @@ export function readPartialSingleLineContent(filePath: string, maxChars: number)
if (remainingChars <= 0) {
streamDestroyed = true
stream.destroy()
- resolve(content)
+ resolve({
+ content,
+ charactersRead: totalRead,
+ totalCharacters,
+ linesRead: hasContent ? currentLine : 0,
+ totalLines,
+ lastLineRead: hasContent ? currentLine : 0,
+ })
return
}
+ let chunkToAdd: string
if (chunkStr.length <= remainingChars) {
- content += chunkStr
+ chunkToAdd = chunkStr
totalRead += chunkStr.length
} else {
- const truncated = chunkStr.substring(0, remainingChars)
- content += truncated
+ chunkToAdd = chunkStr.substring(0, remainingChars)
totalRead += remainingChars
- streamDestroyed = true
- stream.destroy()
- resolve(content)
}
- // Safety check - if we somehow exceed the limit, stop immediately
+ // Mark that we have content
+ if (chunkToAdd.length > 0) {
+ hasContent = true
+ }
+
+ // Count newlines in the chunk we're adding
+ for (let i = 0; i < chunkToAdd.length; i++) {
+ if (chunkToAdd[i] === "\n") {
+ currentLine++
+ }
+ }
+
+ content += chunkToAdd
+
+ // Check if we've reached the character limit
if (totalRead >= maxChars) {
streamDestroyed = true
stream.destroy()
- resolve(content.substring(0, maxChars))
+
+ // Ensure we don't exceed maxChars
+ if (content.length > maxChars) {
+ content = content.substring(0, maxChars)
+ // Recount lines in the final content
+ currentLine = 1
+ hasContent = content.length > 0
+ for (let i = 0; i < content.length; i++) {
+ if (content[i] === "\n") {
+ currentLine++
+ }
+ }
+ }
+
+ resolve({
+ content,
+ charactersRead: Math.min(totalRead, maxChars),
+ totalCharacters,
+ linesRead: hasContent ? currentLine : 0,
+ totalLines,
+ lastLineRead: hasContent ? currentLine : 0,
+ })
}
} catch (error) {
streamDestroyed = true
@@ -70,7 +138,14 @@ export function readPartialSingleLineContent(filePath: string, maxChars: number)
})
stream.on("end", () => {
- resolve(content)
+ resolve({
+ content,
+ charactersRead: totalRead,
+ totalCharacters,
+ linesRead: hasContent ? currentLine : 0,
+ totalLines,
+ lastLineRead: hasContent ? currentLine : 0,
+ })
})
stream.on("error", (error: Error) => {
@@ -78,3 +153,12 @@ export function readPartialSingleLineContent(filePath: string, maxChars: number)
})
})
}
+
+/**
+ * Legacy function for backward compatibility.
+ * @deprecated Use readPartialContent instead
+ */
+export async function readPartialSingleLineContent(filePath: string, maxChars: number): Promise {
+ const result = await readPartialContent(filePath, maxChars)
+ return result.content
+}