diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 7c8dd36fa8..d5e76eccea 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -120,7 +120,6 @@ export const globalSettingsSchema = z.object({ diffEnabled: z.boolean().optional(), fuzzyMatchThreshold: z.number().optional(), experiments: experimentsSchema.optional(), - readFileDeduplicationCacheMinutes: z.number().optional(), codebaseIndexModels: codebaseIndexModelsSchema.optional(), codebaseIndexConfig: codebaseIndexConfigSchema.optional(), diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 8364f0c29c..ecb923400d 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -329,79 +329,6 @@ export class Task extends EventEmitter { return readApiMessages({ taskId: this.taskId, globalStoragePath: this.globalStoragePath }) } - public async getRecentFileContent(filePath: string): Promise { - // Check if the experimental feature is enabled - const state = await this.providerRef.deref()?.getState() - if (!state?.experiments || !experiments.isEnabled(state.experiments, EXPERIMENT_IDS.READ_FILE_DEDUPLICATION)) { - return null - } - - // Get the cache window from settings - const cacheMinutes = state?.readFileDeduplicationCacheMinutes ?? 5 - if (cacheMinutes === 0) { - // Cache is disabled - return null - } - - const cacheWindowMs = cacheMinutes * 60 * 1000 - const now = Date.now() - - // Check recent conversation history for this file - for (let i = this.apiConversationHistory.length - 1; i >= 0; i--) { - const message = this.apiConversationHistory[i] - - // Only process user messages - if (message.role !== "user") { - continue - } - - // Skip messages outside the cache window - if (message.ts && now - message.ts > cacheWindowMs) { - break - } - - // Process content blocks - if (Array.isArray(message.content)) { - for (const block of message.content) { - if (block.type === "text" && typeof block.text === "string") { - // Check for read_file results in text blocks - const readFileMatch = block.text.match(/\[read_file(?:\s+for\s+'([^']+)')?.*?\]\s*Result:/i) - - if (readFileMatch) { - // Extract file paths from the result content - const resultContent = block.text.substring(block.text.indexOf("Result:") + 7).trim() - - // Handle new XML format - const xmlFileMatches = resultContent.matchAll( - /\s*([^<]+)<\/path>[\s\S]*?]*?>([\s\S]*?)<\/content>/g, - ) - for (const match of xmlFileMatches) { - const matchedPath = match[1].trim() - const content = match[2].trim() - if (matchedPath === filePath) { - return content - } - } - - // Handle legacy format (single file) - if ( - readFileMatch[1] && - readFileMatch[1] === filePath && - !resultContent.includes("") - ) { - // For legacy format, the content is directly after "Result:" - // Remove any leading/trailing whitespace - return resultContent.trim() - } - } - } - } - } - } - - return null - } - public async deduplicateReadFileHistory(): Promise { // Check if the experimental feature is enabled const state = await this.providerRef.deref()?.getState() @@ -409,10 +336,6 @@ export class Task extends EventEmitter { return } - // Get the cache window from settings, defaulting to 5 minutes if not set - const cacheMinutes = state?.readFileDeduplicationCacheMinutes ?? 5 - const cacheWindowMs = cacheMinutes * 60 * 1000 - const now = Date.now() const seenFiles = new Map() const blocksToRemove = new Map>() // messageIndex -> Set of blockIndexes to remove @@ -425,11 +348,6 @@ export class Task extends EventEmitter { continue } - // Skip messages within the cache window - if (message.ts && now - message.ts < cacheWindowMs) { - continue - } - // Process content blocks if (Array.isArray(message.content)) { for (let j = 0; j < message.content.length; j++) { diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index b93fbcfa77..9b6bb5908d 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -1611,37 +1611,6 @@ describe("Cline", () => { } }) - it("should preserve messages within cache window", async () => { - const now = Date.now() - cline.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsold content", - }, - ], - ts: now - 10 * 60 * 1000, // 10 minutes ago - }, - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsrecent content", - }, - ], - ts: now - 2 * 60 * 1000, // 2 minutes ago (within 5 minute cache window) - }, - ] - - await cline.deduplicateReadFileHistory() - - // Should keep both messages (recent one is within cache window) - expect(cline.apiConversationHistory).toHaveLength(2) - }) - it("should handle multi-file reads", async () => { const now = Date.now() cline.apiConversationHistory = [ @@ -1944,498 +1913,6 @@ describe("Cline", () => { expect(content[0].text).toContain("new2") } }) - - it("should use configurable cache time limit", async () => { - // Test with 0 minutes (no cache window) - mockProvider.getState.mockResolvedValue({ - experiments: { - [EXPERIMENT_IDS.READ_FILE_DEDUPLICATION]: true, - }, - readFileDeduplicationCacheMinutes: 0, - }) - - const now = Date.now() - cline.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsold content", - }, - ], - ts: now - 1000, // 1 second ago - }, - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsnew content", - }, - ], - ts: now - 500, // 0.5 seconds ago - }, - ] - - await cline.deduplicateReadFileHistory() - - // With 0 cache window, should deduplicate even very recent reads - expect(cline.apiConversationHistory).toHaveLength(1) - const content = cline.apiConversationHistory[0].content - if (Array.isArray(content) && content[0]?.type === "text") { - expect(content[0].text).toContain("new content") - } - }) - - it("should use custom cache time limit from settings", async () => { - // Test with 10 minutes cache window - mockProvider.getState.mockResolvedValue({ - experiments: { - [EXPERIMENT_IDS.READ_FILE_DEDUPLICATION]: true, - }, - readFileDeduplicationCacheMinutes: 10, - }) - - const now = Date.now() - cline.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsold content", - }, - ], - ts: now - 15 * 60 * 1000, // 15 minutes ago - }, - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsrecent content", - }, - ], - ts: now - 8 * 60 * 1000, // 8 minutes ago (within 10 minute window) - }, - ] - - await cline.deduplicateReadFileHistory() - - // Should keep both messages (recent one is within 10 minute cache window) - expect(cline.apiConversationHistory).toHaveLength(2) - }) - - it("should default to 5 minutes when setting is undefined", async () => { - // Test with undefined setting (should default to 5 minutes) - mockProvider.getState.mockResolvedValue({ - experiments: { - [EXPERIMENT_IDS.READ_FILE_DEDUPLICATION]: true, - }, - // readFileDeduplicationCacheMinutes is undefined - }) - - const now = Date.now() - cline.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsold content", - }, - ], - ts: now - 10 * 60 * 1000, // 10 minutes ago - }, - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsrecent content", - }, - ], - ts: now - 3 * 60 * 1000, // 3 minutes ago (within default 5 minute window) - }, - ] - - await cline.deduplicateReadFileHistory() - - // Should keep both messages (recent one is within default 5 minute cache window) - expect(cline.apiConversationHistory).toHaveLength(2) - }) - - it("should handle large cache time limits", async () => { - // Test with 60 minutes (1 hour) cache window - mockProvider.getState.mockResolvedValue({ - experiments: { - [EXPERIMENT_IDS.READ_FILE_DEDUPLICATION]: true, - }, - readFileDeduplicationCacheMinutes: 60, - }) - - const now = Date.now() - cline.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsold content", - }, - ], - ts: now - 2 * 60 * 60 * 1000, // 2 hours ago - }, - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsrecent content", - }, - ], - ts: now - 30 * 60 * 1000, // 30 minutes ago (within 60 minute window) - }, - ] - - await cline.deduplicateReadFileHistory() - - // Should keep both messages (recent one is within 60 minute cache window) - expect(cline.apiConversationHistory).toHaveLength(2) - }) - }) - - describe("getRecentFileContent", () => { - let mockProvider: any - let mockApiConfig: any - let task: Task - - beforeEach(() => { - vi.clearAllMocks() - - mockApiConfig = { - apiProvider: "anthropic", - apiKey: "test-key", - } - - mockProvider = { - context: { - globalStorageUri: { fsPath: "/test/storage" }, - }, - getState: vi.fn().mockResolvedValue({ - experiments: { - [EXPERIMENT_IDS.READ_FILE_DEDUPLICATION]: true, - }, - readFileDeduplicationCacheMinutes: 5, - }), - postStateToWebview: vi.fn().mockResolvedValue(undefined), - postMessageToWebview: vi.fn().mockResolvedValue(undefined), - updateTaskHistory: vi.fn().mockResolvedValue(undefined), - } - - task = new Task({ - provider: mockProvider, - apiConfiguration: mockApiConfig, - task: "test task", - startTask: false, - }) - }) - - it("should return null when feature is disabled", async () => { - mockProvider.getState.mockResolvedValue({ - experiments: { - [EXPERIMENT_IDS.READ_FILE_DEDUPLICATION]: false, - }, - }) - - const now = Date.now() - task.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tstest content", - }, - ], - ts: now - 1000, // 1 second ago - }, - ] - - const result = await task.getRecentFileContent("test.ts") - expect(result).toBeNull() - }) - - it("should return recent file content within cache window", async () => { - const now = Date.now() - task.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsrecent content", - }, - ], - ts: now - 2 * 60 * 1000, // 2 minutes ago (within 5 minute window) - }, - ] - - const result = await task.getRecentFileContent("test.ts") - expect(result).toBe("recent content") - }) - - it("should return null for files outside cache window", async () => { - const now = Date.now() - task.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsold content", - }, - ], - ts: now - 10 * 60 * 1000, // 10 minutes ago (outside 5 minute window) - }, - ] - - const result = await task.getRecentFileContent("test.ts") - expect(result).toBeNull() - }) - - it("should return most recent content when multiple reads exist", async () => { - const now = Date.now() - task.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsold content", - }, - ], - ts: now - 4 * 60 * 1000, // 4 minutes ago - }, - { - role: "assistant", - content: [{ type: "text" as const, text: "Processing..." }], - ts: now - 3 * 60 * 1000, - }, - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsnewer content", - }, - ], - ts: now - 2 * 60 * 1000, // 2 minutes ago - }, - ] - - const result = await task.getRecentFileContent("test.ts") - expect(result).toBe("newer content") - }) - - it("should handle multi-file reads", async () => { - const now = Date.now() - task.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'file1.ts', 'file2.ts'] Result:\nfile1.tscontent1file2.tscontent2", - }, - ], - ts: now - 2 * 60 * 1000, - }, - ] - - const result1 = await task.getRecentFileContent("file1.ts") - expect(result1).toBe("content1") - - const result2 = await task.getRecentFileContent("file2.ts") - expect(result2).toBe("content2") - }) - - it("should return null for non-existent files", async () => { - const now = Date.now() - task.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tstest content", - }, - ], - ts: now - 1000, - }, - ] - - const result = await task.getRecentFileContent("other.ts") - expect(result).toBeNull() - }) - - it("should handle legacy format", async () => { - const now = Date.now() - task.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'legacy.ts'] Result:\nFile content without XML wrapper", - }, - ], - ts: now - 1000, - }, - ] - - const result = await task.getRecentFileContent("legacy.ts") - expect(result).toBe("File content without XML wrapper") - }) - - it("should ignore assistant messages", async () => { - const now = Date.now() - task.apiConversationHistory = [ - { - role: "assistant", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsassistant content", - }, - ], - ts: now - 1000, - }, - ] - - const result = await task.getRecentFileContent("test.ts") - expect(result).toBeNull() - }) - - it("should handle messages without timestamps", async () => { - task.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tstest content", - }, - ], - // No ts property - should be treated as recent - }, - ] - - const result = await task.getRecentFileContent("test.ts") - expect(result).toBe("test content") - }) - - it("should use custom cache time from settings", async () => { - mockProvider.getState.mockResolvedValue({ - experiments: { - [EXPERIMENT_IDS.READ_FILE_DEDUPLICATION]: true, - }, - readFileDeduplicationCacheMinutes: 10, - }) - - const now = Date.now() - task.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tscontent within 10 min", - }, - ], - ts: now - 8 * 60 * 1000, // 8 minutes ago (within 10 minute window) - }, - ] - - const result = await task.getRecentFileContent("test.ts") - expect(result).toBe("content within 10 min") - }) - - it("should handle 0 cache time (no caching)", async () => { - mockProvider.getState.mockResolvedValue({ - experiments: { - [EXPERIMENT_IDS.READ_FILE_DEDUPLICATION]: true, - }, - readFileDeduplicationCacheMinutes: 0, - }) - - const now = Date.now() - task.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for 'test.ts'] Result:\ntest.tsvery recent content", - }, - ], - ts: now - 100, // 0.1 seconds ago - }, - ] - - const result = await task.getRecentFileContent("test.ts") - expect(result).toBeNull() // With 0 cache time, nothing is cached - }) - - it("should handle malformed content gracefully", async () => { - const now = Date.now() - task.apiConversationHistory = [ - { - role: "user", - content: "string content instead of array", // Invalid format - ts: now - 1000, - }, - { - role: "user", - content: [ - { - type: "image" as const, - source: { type: "base64" as const, media_type: "image/png", data: "..." }, - }, - ], // Non-text block - ts: now - 500, - }, - ] - - const result = await task.getRecentFileContent("test.ts") - expect(result).toBeNull() - }) - - it("should handle empty conversation history", async () => { - task.apiConversationHistory = [] - const result = await task.getRecentFileContent("test.ts") - expect(result).toBeNull() - }) - - it("should handle file paths with special characters", async () => { - const now = Date.now() - task.apiConversationHistory = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "[read_file for '@scope/package/file.ts'] Result:\n@scope/package/file.tsscoped content", - }, - ], - ts: now - 1000, - }, - ] - - const result = await task.getRecentFileContent("@scope/package/file.ts") - expect(result).toBe("scoped content") - }) }) }) }) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index d629d3f0df..448425c92b 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -130,9 +130,6 @@ describe("read_file tool with maxReadFileLine setting", () => { // Add the deduplicateReadFileHistory method to the mock mockCline.deduplicateReadFileHistory = vi.fn().mockReturnValue(undefined) - // Add the getRecentFileContent method to the mock - mockCline.getRecentFileContent = vi.fn().mockResolvedValue(null) - toolResult = undefined }) @@ -392,9 +389,6 @@ describe("read_file tool XML output structure", () => { // Add the deduplicateReadFileHistory method to the mock mockCline.deduplicateReadFileHistory = vi.fn().mockReturnValue(undefined) - // Add the getRecentFileContent method to the mock - mockCline.getRecentFileContent = vi.fn().mockResolvedValue(null) - toolResult = undefined }) diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index d9d5d840b0..2e62a82be5 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -431,45 +431,7 @@ export async function readFileTool( const fullPath = path.resolve(cline.cwd, relPath) const { maxReadFileLine = -1 } = (await cline.providerRef.deref()?.getState()) ?? {} - // Check if we have recent content for this file (deduplication) - const recentContent = await cline.getRecentFileContent(relPath) - if (recentContent !== null) { - // We have recent content, use it instead of reading the file again - const lines = recentContent.split("\n") - const totalLines = lines.length - - // Handle range reads - if (fileResult.lineRanges && fileResult.lineRanges.length > 0) { - const rangeResults: string[] = [] - for (const range of fileResult.lineRanges) { - const selectedLines = lines.slice(range.start - 1, range.end).join("\n") - const content = addLineNumbers(selectedLines, range.start) - const lineRangeAttr = ` lines="${range.start}-${range.end}"` - rangeResults.push(`\n${content}`) - } - updateFileResult(relPath, { - xmlContent: `${relPath}\n${rangeResults.join("\n")}\nUsing cached content from recent read\n`, - }) - continue - } - - // Handle normal file read with cached content - const lineRangeAttr = ` lines="1-${totalLines}"` - let xmlInfo = totalLines > 0 ? `\n${recentContent}\n` : `` - - if (totalLines === 0) { - xmlInfo += `File is empty\n` - } else { - xmlInfo += `Using cached content from recent read\n` - } - - updateFileResult(relPath, { - xmlContent: `${relPath}\n${xmlInfo}`, - }) - continue - } - - // Process approved files (no cached content available) + // Process approved files try { const [totalLines, isBinary] = await Promise.all([countFileLines(fullPath), isBinaryFile(fullPath)]) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index e6addc11e1..41e2669148 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1444,9 +1444,6 @@ export class ClineProvider maxDiagnosticMessages, } = state - // Get readFileDeduplicationCacheMinutes with default value - const readFileDeduplicationCacheMinutes = state.readFileDeduplicationCacheMinutes ?? 5 - const telemetryKey = process.env.POSTHOG_API_KEY const machineId = vscode.env.machineId const mergedAllowedCommands = this.mergeAllowedCommands(allowedCommands) @@ -1536,7 +1533,6 @@ export class ClineProvider language: language ?? formatLanguage(vscode.env.language), renderContext: this.renderContext, maxReadFileLine: maxReadFileLine ?? -1, - readFileDeduplicationCacheMinutes: readFileDeduplicationCacheMinutes ?? 5, maxConcurrentFileReads: maxConcurrentFileReads ?? 5, settingsImportedAt: this.settingsImportedAt, terminalCompressProgressBar: terminalCompressProgressBar ?? true, @@ -1707,7 +1703,6 @@ export class ClineProvider telemetrySetting: stateValues.telemetrySetting || "unset", showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true, maxReadFileLine: stateValues.maxReadFileLine ?? -1, - readFileDeduplicationCacheMinutes: stateValues.readFileDeduplicationCacheMinutes ?? 5, maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5, historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, cloudUserInfo, diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 1fa05dd81f..344b098816 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -542,7 +542,6 @@ describe("ClineProvider", () => { profileThresholds: {}, hasOpenedModeSelector: false, diagnosticsEnabled: true, - readFileDeduplicationCacheMinutes: 5, } const message: ExtensionMessage = { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 582906226c..816069f91f 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -281,7 +281,6 @@ export type ExtensionState = Pick< maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500) showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings maxReadFileLine: number // Maximum number of lines to read from a file before truncating - readFileDeduplicationCacheMinutes: number // Cache window in minutes for read_file deduplication (0 = no cache) experiments: Experiments // Map of experiment IDs to their enabled state diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d222dd65b8..1304e4c7d5 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -202,7 +202,6 @@ export interface WebviewMessage { | "saveCodeIndexSettingsAtomic" | "requestCodeIndexSecretStatus" | "requestCommands" - | "readFileDeduplicationCacheMinutes" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"