feat: Add file context tracking system (#2440)

* feat: Add file context tracking system

This commit adds a comprehensive file context tracking system that monitors file operations (reads, edits) by both Roo and users. The system helps prevent stale context issues and improves checkpoint management.

Key features:
- Track files accessed via tools, mentions, or edits
- Monitor file changes outside of Roo using file watchers
- Store file operation metadata with timestamps
- Trigger checkpoints automatically when files are modified
- Prevent false positives by distinguishing between Roo and user edits

The implementation includes:
- New FileContextTracker class to manage file operations
- Type definitions for file metadata tracking
- Integration with all file-related tools
- File mention tracking in the mentions system
- Improved checkpoint triggering based on file modifications

* Update src/core/context-tracking/FileContextTracker.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update src/core/context-tracking/FileContextTracker.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* test: Add mocks for getFileContextTracker in Cline tests

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
Sam Hoang Van 2025-04-10 21:56:52 +07:00 committed by GitHub
parent 0ddfa4d0bf
commit 5352beb95c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 410 additions and 66 deletions

View file

@ -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<ClineEvents> {
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<ClineEvents> {
throw new Error("Either historyItem or task/images must be provided")
}
this.rooIgnoreController = new RooIgnoreController(this.cwd)
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.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.apiConfiguration = apiConfiguration
this.api = buildApiHandler(apiConfiguration)
this.urlContentFetcher = new UrlContentFetcher(provider.context)
@ -929,6 +932,7 @@ export class Cline extends EventEmitter<ClineEvents> {
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<ClineEvents> {
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<ClineEvents> {
// 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<ClineEvents> {
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<ClineEvents> {
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<ClineEvents> {
)
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<ClineEvents> {
}
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 "<feedback>" (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (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("<task>") || text.includes("<feedback>")
// 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 "<feedback>" (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (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("<task>") || text.includes("<feedback>")
if (block.type === "text") {
if (shouldProcessMentions(block.text)) {
return {
...block,
text: await parseMentions(block.text, this.cwd, this.urlContentFetcher),
}
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,
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,
),
}
}
return contentBlock
}),
)
return {
...block,
content: parsedContent,
}
}
return block
}
return block
}),
),
this.getEnvironmentDetails(includeFileDetails),
])
} 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,
this.fileContextTracker,
),
}
}
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,
this.fileContextTracker,
),
}
}
return contentBlock
}),
)
return {
...block,
content: parsedContent,
}
}
return block
}
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<ClineEvents> {
// 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<ClineEvents> {
this.enableCheckpoints = false
}
}
// Public accessor for fileContextTracker
public getFileContextTracker(): FileContextTracker {
return this.fileContextTracker
}
}

View file

@ -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", () => {
"<task>Text with @/some/path in task tags</task>",
expect.any(String),
expect.any(Object),
expect.any(Object),
)
// Feedback tag content should be processed
@ -951,6 +962,7 @@ describe("Cline", () => {
"<feedback>Check @/some/path</feedback>",
expect.any(String),
expect.any(Object),
expect.any(Object),
)
// Regular tool result should not be processed

View file

@ -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

View file

@ -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

View file

@ -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<ClineProvider>
// File tracking and watching
private fileWatchers = new Map<string, vscode.FileSystemWatcher>()
private recentlyModifiedFiles = new Set<string>()
private recentlyEditedByRoo = new Set<string>()
private checkpointPossibleFiles = new Set<string>()
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<TaskMetadata> {
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()
}
}

View file

@ -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<typeof recordSourceSchema>
// 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<typeof fileMetadataEntrySchema>
// 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<typeof taskMetadataSchema>

View file

@ -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<void> {
if (!mention) {
@ -38,7 +39,12 @@ export async function openMention(mention?: string): Promise<void> {
}
}
export async function parseMentions(text: string, cwd: string, urlContentFetcher: UrlContentFetcher): Promise<string> {
export async function parseMentions(
text: string,
cwd: string,
urlContentFetcher: UrlContentFetcher,
fileContextTracker?: FileContextTracker,
): Promise<string> {
const mentions: Set<string> = 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<folder_content path="${mentionPath}">\n${content}\n</folder_content>`
} else {
parsedText += `\n\n<file_content path="${mentionPath}">\n${content}\n</file_content>`
// Track that this file was mentioned and its content was included
if (fileContextTracker) {
await fileContextTracker.trackFileContext(mentionPath, "file_mentioned")
}
}
} catch (error) {
if (mention.endsWith("/")) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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
}

View file

@ -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 = `<content${lineRangeAttr}>\n${content}</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 = `<file><path>${relPath}</path>\n${contentTag}${xmlInfo}</file>`
pushToolResult(xmlResult)

View file

@ -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(

View file

@ -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(

View file

@ -7,4 +7,5 @@ export const GlobalFileNames = {
mcpSettings: "mcp_settings.json",
unboundModels: "unbound_models.json",
customModes: "custom_modes.json",
taskMetadata: "task_metadata.json",
}