diff --git a/src/core/Cline.ts b/src/core/Cline.ts index e32dca97e79..ea5e231a18a 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -53,6 +53,7 @@ import { calculateApiCostAnthropic } from "../utils/cost" import { fileExistsAtPath } from "../utils/fs" import { arePathsEqual } from "../utils/path" import { parseMentions } from "./mentions" +import { FileContextTracker } from "./context-tracking/FileContextTracker" import { RooIgnoreController } from "./ignore/RooIgnoreController" import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message" import { formatResponse } from "./prompts/responses" @@ -130,6 +131,7 @@ export class Cline extends EventEmitter { readonly apiConfiguration: ApiConfiguration api: ApiHandler + private fileContextTracker: FileContextTracker private urlContentFetcher: UrlContentFetcher browserSession: BrowserSession didEditFile: boolean = false @@ -201,14 +203,15 @@ export class Cline extends EventEmitter { throw new Error("Either historyItem or task/images must be provided") } + this.taskId = historyItem ? historyItem.id : crypto.randomUUID() + this.instanceId = crypto.randomUUID().slice(0, 8) + this.taskNumber = -1 + this.rooIgnoreController = new RooIgnoreController(this.cwd) + this.fileContextTracker = new FileContextTracker(provider, this.taskId) this.rooIgnoreController.initialize().catch((error) => { console.error("Failed to initialize RooIgnoreController:", error) }) - - this.taskId = historyItem ? historyItem.id : crypto.randomUUID() - this.instanceId = crypto.randomUUID().slice(0, 8) - this.taskNumber = -1 this.apiConfiguration = apiConfiguration this.api = buildApiHandler(apiConfiguration) this.urlContentFetcher = new UrlContentFetcher(provider.context) @@ -929,6 +932,7 @@ export class Cline extends EventEmitter { this.urlContentFetcher.closeBrowser() this.browserSession.closeBrowser() this.rooIgnoreController?.dispose() + this.fileContextTracker.dispose() // If we're not streaming then `abortStream` (which reverts the diff // view changes) won't be called, so we need to revert the changes here. @@ -1322,8 +1326,6 @@ export class Cline extends EventEmitter { const block = cloneDeep(this.assistantMessageContent[this.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too - let isCheckpointPossible = false - switch (block.type) { case "text": { if (this.didRejectTool || this.didAlreadyUseTool) { @@ -1460,7 +1462,6 @@ export class Cline extends EventEmitter { // Flag a checkpoint as possible since we've used a tool // which may have changed the file system. - isCheckpointPossible = true } const askApproval = async ( @@ -1583,6 +1584,7 @@ export class Cline extends EventEmitter { break case "read_file": await readFileTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag) + break case "fetch_instructions": await fetchInstructionsTool(this, block, askApproval, handleError, pushToolResult) @@ -1662,7 +1664,9 @@ export class Cline extends EventEmitter { break } - if (isCheckpointPossible) { + const recentlyModifiedFiles = this.fileContextTracker.getAndClearCheckpointPossibleFile() + if (recentlyModifiedFiles.length > 0) { + // TODO: we can track what file changes were made and only checkpoint those files, this will be save storage this.checkpointSave() } @@ -1783,18 +1787,17 @@ export class Cline extends EventEmitter { ) const [parsedUserContent, environmentDetails] = await this.loadContext(userContent, includeFileDetails) - userContent = parsedUserContent // add environment details as its own text block, separate from tool results - userContent.push({ type: "text", text: environmentDetails }) + const finalUserContent = [...parsedUserContent, { type: "text", text: environmentDetails }] as UserContent - await this.addToApiConversationHistory({ role: "user", content: userContent }) + await this.addToApiConversationHistory({ role: "user", content: finalUserContent }) telemetryService.captureConversationMessage(this.taskId, "user") // since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started") this.clineMessages[lastApiReqIndex].text = JSON.stringify({ - request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"), + request: finalUserContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"), } satisfies ClineApiReqInfo) await this.saveClineMessages() @@ -2045,62 +2048,73 @@ export class Cline extends EventEmitter { } async loadContext(userContent: UserContent, includeFileDetails: boolean = false) { - return await Promise.all([ - // Process userContent array, which contains various block types: - // TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam. - // We need to apply parseMentions() to: - // 1. All TextBlockParam's text (first user message with task) - // 2. ToolResultBlockParam's content/context text arrays if it contains "" (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions) - Promise.all( - userContent.map(async (block) => { - const shouldProcessMentions = (text: string) => - text.includes("") || text.includes("") - - if (block.type === "text") { - if (shouldProcessMentions(block.text)) { + // Process userContent array, which contains various block types: + // TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam. + // We need to apply parseMentions() to: + // 1. All TextBlockParam's text (first user message with task) + // 2. ToolResultBlockParam's content/context text arrays if it contains "" (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions) + const parsedUserContent = await Promise.all( + userContent.map(async (block) => { + const shouldProcessMentions = (text: string) => text.includes("") || text.includes("") + + if (block.type === "text") { + if (shouldProcessMentions(block.text)) { + return { + ...block, + text: await parseMentions( + block.text, + this.cwd, + this.urlContentFetcher, + this.fileContextTracker, + ), + } + } + return block + } else if (block.type === "tool_result") { + if (typeof block.content === "string") { + if (shouldProcessMentions(block.content)) { return { ...block, - text: await parseMentions(block.text, this.cwd, this.urlContentFetcher), + content: await parseMentions( + block.content, + this.cwd, + this.urlContentFetcher, + this.fileContextTracker, + ), } } return block - } else if (block.type === "tool_result") { - if (typeof block.content === "string") { - if (shouldProcessMentions(block.content)) { - return { - ...block, - content: await parseMentions(block.content, this.cwd, this.urlContentFetcher), - } - } - return block - } else if (Array.isArray(block.content)) { - const parsedContent = await Promise.all( - block.content.map(async (contentBlock) => { - if (contentBlock.type === "text" && shouldProcessMentions(contentBlock.text)) { - return { - ...contentBlock, - text: await parseMentions( - contentBlock.text, - this.cwd, - this.urlContentFetcher, - ), - } + } else if (Array.isArray(block.content)) { + const parsedContent = await Promise.all( + block.content.map(async (contentBlock) => { + if (contentBlock.type === "text" && shouldProcessMentions(contentBlock.text)) { + return { + ...contentBlock, + text: await parseMentions( + contentBlock.text, + this.cwd, + this.urlContentFetcher, + this.fileContextTracker, + ), } - return contentBlock - }), - ) - return { - ...block, - content: parsedContent, - } + } + return contentBlock + }), + ) + return { + ...block, + content: parsedContent, } - return block } return block - }), - ), - this.getEnvironmentDetails(includeFileDetails), - ]) + } + return block + }), + ) + + const environmentDetails = await this.getEnvironmentDetails(includeFileDetails) + + return [parsedUserContent, environmentDetails] } async getEnvironmentDetails(includeFileDetails: boolean = false) { @@ -2251,6 +2265,16 @@ export class Cline extends EventEmitter { // details += "\n(No errors detected)" // } + // Add recently modified files section + const recentlyModifiedFiles = this.fileContextTracker.getAndClearRecentlyModifiedFiles() + if (recentlyModifiedFiles.length > 0) { + details += + "\n\n# Recently Modified Files\nThese files have been modified since you last accessed them (file was just edited so you may need to re-read it before editing):" + for (const filePath of recentlyModifiedFiles) { + details += `\n${filePath}` + } + } + if (terminalDetails) { details += terminalDetails } @@ -2619,4 +2643,9 @@ export class Cline extends EventEmitter { this.enableCheckpoints = false } } + + // Public accessor for fileContextTracker + public getFileContextTracker(): FileContextTracker { + return this.fileContextTracker + } } diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts index 30680854256..90e365caf16 100644 --- a/src/core/__tests__/Cline.test.ts +++ b/src/core/__tests__/Cline.test.ts @@ -16,6 +16,16 @@ import { ApiStreamChunk } from "../../api/transform/stream" // Mock RooIgnoreController jest.mock("../ignore/RooIgnoreController") +// Mock storagePathManager to prevent dynamic import issues +jest.mock("../../shared/storagePathManager", () => ({ + getTaskDirectoryPath: jest.fn().mockImplementation((globalStoragePath, taskId) => { + return Promise.resolve(`${globalStoragePath}/tasks/${taskId}`) + }), + getSettingsDirectoryPath: jest.fn().mockImplementation((globalStoragePath) => { + return Promise.resolve(`${globalStoragePath}/settings`) + }), +})) + // Mock fileExistsAtPath jest.mock("../../utils/fs", () => ({ fileExistsAtPath: jest.fn().mockImplementation((filePath) => { @@ -941,6 +951,7 @@ describe("Cline", () => { "Text with @/some/path in task tags", expect.any(String), expect.any(Object), + expect.any(Object), ) // Feedback tag content should be processed @@ -951,6 +962,7 @@ describe("Cline", () => { "Check @/some/path", expect.any(String), expect.any(Object), + expect.any(Object), ) // Regular tool result should not be processed diff --git a/src/core/__tests__/read-file-maxReadFileLine.test.ts b/src/core/__tests__/read-file-maxReadFileLine.test.ts index d668ce333b0..0f3e3a0d67e 100644 --- a/src/core/__tests__/read-file-maxReadFileLine.test.ts +++ b/src/core/__tests__/read-file-maxReadFileLine.test.ts @@ -122,6 +122,9 @@ describe("read_file tool with maxReadFileLine setting", () => { mockCline.say = jest.fn().mockResolvedValue(undefined) mockCline.ask = jest.fn().mockResolvedValue(true) mockCline.presentAssistantMessage = jest.fn() + mockCline.getFileContextTracker = jest.fn().mockReturnValue({ + trackFileContext: jest.fn().mockResolvedValue(undefined), + }) // Reset tool result toolResult = undefined diff --git a/src/core/__tests__/read-file-xml.test.ts b/src/core/__tests__/read-file-xml.test.ts index dda287376af..6b995d18b8e 100644 --- a/src/core/__tests__/read-file-xml.test.ts +++ b/src/core/__tests__/read-file-xml.test.ts @@ -114,6 +114,10 @@ describe("read_file tool XML output structure", () => { mockCline.ask = jest.fn().mockResolvedValue(true) mockCline.presentAssistantMessage = jest.fn() mockCline.sayAndCreateMissingParamError = jest.fn().mockResolvedValue("Missing required parameter") + // Add mock for getFileContextTracker method + mockCline.getFileContextTracker = jest.fn().mockReturnValue({ + trackFileContext: jest.fn().mockResolvedValue(undefined), + }) // Reset tool result toolResult = undefined diff --git a/src/core/context-tracking/FileContextTracker.ts b/src/core/context-tracking/FileContextTracker.ts new file mode 100644 index 00000000000..4177d989154 --- /dev/null +++ b/src/core/context-tracking/FileContextTracker.ts @@ -0,0 +1,225 @@ +import * as path from "path" +import * as vscode from "vscode" +import { getTaskDirectoryPath } from "../../shared/storagePathManager" +import { GlobalFileNames } from "../../shared/globalFileNames" +import { fileExistsAtPath } from "../../utils/fs" +import fs from "fs/promises" +import { ContextProxy } from "../config/ContextProxy" +import type { FileMetadataEntry, RecordSource, TaskMetadata } from "./FileContextTrackerTypes" +import { ClineProvider } from "../webview/ClineProvider" + +// This class is responsible for tracking file operations that may result in stale context. +// If a user modifies a file outside of Roo, the context may become stale and need to be updated. +// We do not want Roo to reload the context every time a file is modified, so we use this class merely +// to inform Roo that the change has occurred, and tell Roo to reload the file before making +// any changes to it. This fixes an issue with diff editing, where Roo was unable to complete a diff edit. + +// FileContextTracker +// +// This class is responsible for tracking file operations. +// If the full contents of a file are passed to Roo via a tool, mention, or edit, the file is marked as active. +// If a file is modified outside of Roo, we detect and track this change to prevent stale context. +export class FileContextTracker { + readonly taskId: string + private providerRef: WeakRef + + // File tracking and watching + private fileWatchers = new Map() + private recentlyModifiedFiles = new Set() + private recentlyEditedByRoo = new Set() + private checkpointPossibleFiles = new Set() + + constructor(provider: ClineProvider, taskId: string) { + this.providerRef = new WeakRef(provider) + this.taskId = taskId + } + + // Gets the current working directory or returns undefined if it cannot be determined + private getCwd(): string | undefined { + const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) + if (!cwd) { + console.info("No workspace folder available - cannot determine current working directory") + } + return cwd + } + + // File watchers are set up for each file that is tracked in the task metadata. + async setupFileWatcher(filePath: string) { + // Only setup watcher if it doesn't already exist for this file + if (this.fileWatchers.has(filePath)) { + return + } + + const cwd = this.getCwd() + if (!cwd) { + return + } + + // Create a file system watcher for this specific file + const fileUri = vscode.Uri.file(path.resolve(cwd, filePath)) + const watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(path.dirname(fileUri.fsPath), path.basename(fileUri.fsPath)), + ) + + // Track file changes + watcher.onDidChange(() => { + if (this.recentlyEditedByRoo.has(filePath)) { + this.recentlyEditedByRoo.delete(filePath) // This was an edit by Roo, no need to inform Roo + } else { + this.recentlyModifiedFiles.add(filePath) // This was a user edit, we will inform Roo + this.trackFileContext(filePath, "user_edited") // Update the task metadata with file tracking + } + }) + + // Store the watcher so we can dispose it later + this.fileWatchers.set(filePath, watcher) + } + + // Tracks a file operation in metadata and sets up a watcher for the file + // This is the main entry point for FileContextTracker and is called when a file is passed to Roo via a tool, mention, or edit. + async trackFileContext(filePath: string, operation: RecordSource) { + try { + const cwd = this.getCwd() + if (!cwd) { + return + } + + await this.addFileToFileContextTracker(this.taskId, filePath, operation) + + // Set up file watcher for this file + await this.setupFileWatcher(filePath) + } catch (error) { + console.error("Failed to track file operation:", error) + } + } + + public getContextProxy(): ContextProxy | undefined { + const provider = this.providerRef.deref() + if (!provider) { + console.error("ClineProvider reference is no longer valid") + return undefined + } + const context = provider.contextProxy + + if (!context) { + console.error("Context is not available") + return undefined + } + + return context + } + + // Gets task metadata from storage + async getTaskMetadata(taskId: string): Promise { + const globalStoragePath = this.getContextProxy()?.globalStorageUri.fsPath ?? '' + const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) + const filePath = path.join(taskDir, GlobalFileNames.taskMetadata) + try { + if (await fileExistsAtPath(filePath)) { + return JSON.parse(await fs.readFile(filePath, "utf8")) + } + } catch (error) { + console.error("Failed to read task metadata:", error) + } + return { files_in_context: [] } + } + + // Saves task metadata to storage + async saveTaskMetadata(taskId: string, metadata: TaskMetadata) { + try { + const globalStoragePath = this.getContextProxy()!.globalStorageUri.fsPath + const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) + const filePath = path.join(taskDir, GlobalFileNames.taskMetadata) + await fs.writeFile(filePath, JSON.stringify(metadata, null, 2)) + } catch (error) { + console.error("Failed to save task metadata:", error) + } + } + + // Adds a file to the metadata tracker + // This handles the business logic of determining if the file is new, stale, or active. + // It also updates the metadata with the latest read/edit dates. + async addFileToFileContextTracker(taskId: string, filePath: string, source: RecordSource) { + try { + const metadata = await this.getTaskMetadata(taskId) + const now = Date.now() + + // Mark existing entries for this file as stale + metadata.files_in_context.forEach((entry) => { + if (entry.path === filePath && entry.record_state === "active") { + entry.record_state = "stale" + } + }) + + // Helper to get the latest date for a specific field and file + const getLatestDateForField = (path: string, field: keyof FileMetadataEntry): number | null => { + const relevantEntries = metadata.files_in_context + .filter((entry) => entry.path === path && entry[field]) + .sort((a, b) => (b[field] as number) - (a[field] as number)) + + return relevantEntries.length > 0 ? (relevantEntries[0][field] as number) : null + } + + let newEntry: FileMetadataEntry = { + path: filePath, + record_state: "active", + record_source: source, + roo_read_date: getLatestDateForField(filePath, "roo_read_date"), + roo_edit_date: getLatestDateForField(filePath, "roo_edit_date"), + user_edit_date: getLatestDateForField(filePath, "user_edit_date"), + } + + switch (source) { + // user_edited: The user has edited the file + case "user_edited": + newEntry.user_edit_date = now + this.recentlyModifiedFiles.add(filePath) + break + + // roo_edited: Roo has edited the file + case "roo_edited": + newEntry.roo_read_date = now + newEntry.roo_edit_date = now + this.checkpointPossibleFiles.add(filePath) + break + + // read_tool/file_mentioned: Roo has read the file via a tool or file mention + case "read_tool": + case "file_mentioned": + newEntry.roo_read_date = now + break + } + + metadata.files_in_context.push(newEntry) + await this.saveTaskMetadata(taskId, metadata) + } catch (error) { + console.error("Failed to add file to metadata:", error) + } + } + + // Returns (and then clears) the set of recently modified files + getAndClearRecentlyModifiedFiles(): string[] { + const files = Array.from(this.recentlyModifiedFiles) + this.recentlyModifiedFiles.clear() + return files + } + + getAndClearCheckpointPossibleFile(): string[] { + const files = Array.from(this.checkpointPossibleFiles) + this.checkpointPossibleFiles.clear() + return files + } + + // Marks a file as edited by Roo to prevent false positives in file watchers + markFileAsEditedByRoo(filePath: string): void { + this.recentlyEditedByRoo.add(filePath) + } + + // Disposes all file watchers + dispose(): void { + for (const watcher of this.fileWatchers.values()) { + watcher.dispose() + } + this.fileWatchers.clear() + } +} diff --git a/src/core/context-tracking/FileContextTrackerTypes.ts b/src/core/context-tracking/FileContextTrackerTypes.ts new file mode 100644 index 00000000000..7a761a1d39c --- /dev/null +++ b/src/core/context-tracking/FileContextTrackerTypes.ts @@ -0,0 +1,28 @@ +import { z } from "zod" + +// Zod schema for RecordSource +export const recordSourceSchema = z.enum(["read_tool", "user_edited", "roo_edited", "file_mentioned"]) + +// TypeScript type derived from the Zod schema +export type RecordSource = z.infer + +// Zod schema for FileMetadataEntry +export const fileMetadataEntrySchema = z.object({ + path: z.string(), + record_state: z.enum(["active", "stale"]), + record_source: recordSourceSchema, + roo_read_date: z.number().nullable(), + roo_edit_date: z.number().nullable(), + user_edit_date: z.number().nullable().optional(), +}) + +// TypeScript type derived from the Zod schema +export type FileMetadataEntry = z.infer + +// Zod schema for TaskMetadata +export const taskMetadataSchema = z.object({ + files_in_context: z.array(fileMetadataEntrySchema), +}) + +// TypeScript type derived from the Zod schema +export type TaskMetadata = z.infer diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index d32b1ec08d5..592ff8fe87e 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -10,6 +10,7 @@ import { diagnosticsToProblemsString } from "../../integrations/diagnostics" import { getCommitInfo, getWorkingState } from "../../utils/git" import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output" import { getWorkspacePath } from "../../utils/path" +import { FileContextTracker } from "../context-tracking/FileContextTracker" export async function openMention(mention?: string): Promise { if (!mention) { @@ -38,7 +39,12 @@ export async function openMention(mention?: string): Promise { } } -export async function parseMentions(text: string, cwd: string, urlContentFetcher: UrlContentFetcher): Promise { +export async function parseMentions( + text: string, + cwd: string, + urlContentFetcher: UrlContentFetcher, + fileContextTracker?: FileContextTracker, +): Promise { const mentions: Set = new Set() let parsedText = text.replace(mentionRegexGlobal, (match, mention) => { mentions.add(mention) @@ -95,6 +101,10 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher parsedText += `\n\n\n${content}\n` } else { parsedText += `\n\n\n${content}\n` + // Track that this file was mentioned and its content was included + if (fileContextTracker) { + await fileContextTracker.trackFileContext(mentionPath, "file_mentioned") + } } } catch (error) { if (mention.endsWith("/")) { diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts index a20bace0971..d8ae9fa610d 100644 --- a/src/core/tools/applyDiffTool.ts +++ b/src/core/tools/applyDiffTool.ts @@ -9,6 +9,7 @@ import { fileExistsAtPath } from "../../utils/fs" import { addLineNumbers } from "../../integrations/misc/extract-text" import path from "path" import fs from "fs/promises" +import { RecordSource } from "../context-tracking/FileContextTrackerTypes" export async function applyDiffTool( cline: Cline, @@ -138,6 +139,10 @@ export async function applyDiffTool( } const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges() + // Track file edit operation + if (relPath) { + await cline.getFileContextTracker().trackFileContext(relPath, "roo_edited" as RecordSource) + } cline.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request let partFailHint = "" if (diffResult.failParts && diffResult.failParts.length > 0) { diff --git a/src/core/tools/insertContentTool.ts b/src/core/tools/insertContentTool.ts index 9ff2b28429d..24cf6c57b64 100644 --- a/src/core/tools/insertContentTool.ts +++ b/src/core/tools/insertContentTool.ts @@ -5,6 +5,7 @@ import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./ty import { formatResponse } from "../prompts/responses" import { ClineSayTool } from "../../shared/ExtensionMessage" import path from "path" +import { RecordSource } from "../context-tracking/FileContextTrackerTypes" import { fileExistsAtPath } from "../../utils/fs" import { insertGroups } from "../diff/insert-groups" import delay from "delay" @@ -127,6 +128,11 @@ export async function insertContentTool( } const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges() + + // Track file edit operation + if (relPath) { + await cline.getFileContextTracker().trackFileContext(relPath, "roo_edited" as RecordSource) + } cline.didEditFile = true if (!userEdits) { diff --git a/src/core/tools/listCodeDefinitionNamesTool.ts b/src/core/tools/listCodeDefinitionNamesTool.ts index 46b8afae2b1..6d6a2db3e9c 100644 --- a/src/core/tools/listCodeDefinitionNamesTool.ts +++ b/src/core/tools/listCodeDefinitionNamesTool.ts @@ -7,6 +7,7 @@ import { getReadablePath } from "../../utils/path" import path from "path" import fs from "fs/promises" import { parseSourceCodeForDefinitionsTopLevel, parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" +import { RecordSource } from "../context-tracking/FileContextTrackerTypes" export async function listCodeDefinitionNamesTool( cline: Cline, @@ -59,6 +60,9 @@ export async function listCodeDefinitionNamesTool( if (!didApprove) { return } + if (relPath) { + await cline.getFileContextTracker().trackFileContext(relPath, "read_tool" as RecordSource) + } pushToolResult(result) return } diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index 0ff345518de..2a3fc6cca21 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -5,6 +5,7 @@ import { ToolUse } from "../assistant-message" import { formatResponse } from "../prompts/responses" import { t } from "../../i18n" import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types" +import { RecordSource } from "../context-tracking/FileContextTrackerTypes" import { isPathOutsideWorkspace } from "../../utils/pathUtils" import { getReadablePath } from "../../utils/path" import { countFileLines } from "../../integrations/misc/line-counter" @@ -216,6 +217,11 @@ export async function readFileTool( contentTag = `\n${content}\n` } + // Track file read operation + if (relPath) { + await cline.getFileContextTracker().trackFileContext(relPath, "read_tool" as RecordSource) + } + // Format the result into the required XML structure const xmlResult = `${relPath}\n${contentTag}${xmlInfo}` pushToolResult(xmlResult) diff --git a/src/core/tools/searchAndReplaceTool.ts b/src/core/tools/searchAndReplaceTool.ts index 3b204273bb1..6996c9361e8 100644 --- a/src/core/tools/searchAndReplaceTool.ts +++ b/src/core/tools/searchAndReplaceTool.ts @@ -8,6 +8,7 @@ import path from "path" import { fileExistsAtPath } from "../../utils/fs" import { addLineNumbers } from "../../integrations/misc/extract-text" import fs from "fs/promises" +import { RecordSource } from "../context-tracking/FileContextTrackerTypes" export async function searchAndReplaceTool( cline: Cline, @@ -143,6 +144,10 @@ export async function searchAndReplaceTool( } const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges() + if (relPath) { + await cline.getFileContextTracker().trackFileContext(relPath, "roo_edited" as RecordSource) + } + cline.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request if (userEdits) { await cline.say( diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index 5c24584b91c..25f3a72df29 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -5,6 +5,7 @@ import { ClineSayTool } from "../../shared/ExtensionMessage" import { ToolUse } from "../assistant-message" import { formatResponse } from "../prompts/responses" import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types" +import { RecordSource } from "../context-tracking/FileContextTrackerTypes" import path from "path" import { fileExistsAtPath } from "../../utils/fs" import { addLineNumbers, stripLineNumbers } from "../../integrations/misc/extract-text" @@ -173,6 +174,11 @@ export async function writeToFileTool( return } const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges() + + // Track file edit operation + if (relPath) { + await cline.getFileContextTracker().trackFileContext(relPath, "roo_edited" as RecordSource) + } cline.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request if (userEdits) { await cline.say( diff --git a/src/shared/globalFileNames.ts b/src/shared/globalFileNames.ts index f26174d224c..68990dfe955 100644 --- a/src/shared/globalFileNames.ts +++ b/src/shared/globalFileNames.ts @@ -7,4 +7,5 @@ export const GlobalFileNames = { mcpSettings: "mcp_settings.json", unboundModels: "unbound_models.json", customModes: "custom_modes.json", + taskMetadata: "task_metadata.json", }