diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index f3def9d24f..c60e803134 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -221,29 +221,80 @@ async function checkGitInstallation( try { const checkpointFileChangeManager = provider?.getFileChangeManager() if (checkpointFileChangeManager) { - // Get the initial baseline (preserve for cumulative diff tracking) - const initialBaseline = checkpointFileChangeManager.getChanges().baseCheckpoint + // Get the current baseline for cumulative tracking + let currentBaseline = checkpointFileChangeManager.getChanges().baseCheckpoint + + // For cumulative tracking, we want to calculate from baseline to new checkpoint + // But if this is the first time or baseline is invalid, update it to fromHash + try { + await service.getDiff({ from: currentBaseline, to: currentBaseline }) + log( + `[Task#checkpointCreated] Using existing baseline ${currentBaseline} for cumulative tracking`, + ) + } catch (baselineValidationError) { + // Baseline is invalid, use fromHash as the new baseline for cumulative tracking + log( + `[Task#checkpointCreated] Baseline validation failed for ${currentBaseline}: ${baselineValidationError instanceof Error ? baselineValidationError.message : String(baselineValidationError)}`, + ) + log(`[Task#checkpointCreated] Updating baseline to fromHash: ${fromHash}`) + currentBaseline = fromHash + // Update FileChangeManager baseline to match + try { + await checkpointFileChangeManager.updateBaseline(currentBaseline) + log(`[Task#checkpointCreated] Successfully updated baseline to ${currentBaseline}`) + } catch (updateError) { + log( + `[Task#checkpointCreated] Failed to update baseline: ${updateError instanceof Error ? updateError.message : String(updateError)}`, + ) + throw updateError + } + } + log( - `[Task#checkpointCreated] Calculating cumulative changes from initial baseline ${initialBaseline} to ${toHash}`, + `[Task#checkpointCreated] Calculating cumulative changes from baseline ${currentBaseline} to ${toHash}`, ) - // Calculate cumulative diff from initial baseline to new checkpoint using checkpoint service - const changes = await service.getDiff({ from: initialBaseline, to: toHash }) + // Calculate cumulative diff from baseline to new checkpoint using checkpoint service + const changes = await service.getDiff({ from: currentBaseline, to: toHash }) if (changes && changes.length > 0) { // Convert to FileChange format with correct checkpoint references - const fileChanges = changes.map((change: any) => ({ - uri: change.paths.relative, - type: (change.paths.newFile - ? "create" - : change.paths.deletedFile - ? "delete" - : "edit") as FileChangeType, - fromCheckpoint: initialBaseline, // Always reference initial baseline for cumulative view - toCheckpoint: toHash, // Current checkpoint for comparison - linesAdded: change.content.after ? change.content.after.split("\n").length : 0, - linesRemoved: change.content.before ? change.content.before.split("\n").length : 0, - })) + const fileChanges = changes.map((change: any) => { + const type = ( + change.paths.newFile ? "create" : change.paths.deletedFile ? "delete" : "edit" + ) as FileChangeType + + // Calculate actual line differences for the change + let linesAdded = 0 + let linesRemoved = 0 + + if (type === "create") { + // New file: all lines are added + linesAdded = change.content.after ? change.content.after.split("\n").length : 0 + linesRemoved = 0 + } else if (type === "delete") { + // Deleted file: all lines are removed + linesAdded = 0 + linesRemoved = change.content.before ? change.content.before.split("\n").length : 0 + } else { + // Modified file: use FileChangeManager's improved calculation method + const lineDifferences = FileChangeManager.calculateLineDifferences( + change.content.before || "", + change.content.after || "", + ) + linesAdded = lineDifferences.linesAdded + linesRemoved = lineDifferences.linesRemoved + } + + return { + uri: change.paths.relative, + type, + fromCheckpoint: currentBaseline, // Reference current baseline for cumulative view + toCheckpoint: toHash, // Current checkpoint for comparison + linesAdded, + linesRemoved, + } + }) log(`[Task#checkpointCreated] Found ${fileChanges.length} cumulative file changes`) @@ -253,13 +304,13 @@ async function checkGitInstallation( // DON'T clear accepted/rejected state here - preserve user's accept/reject decisions // The state should only be cleared on baseline changes (checkpoint restore) or task restart - // Get filtered changeset that excludes already accepted/rejected files and only shows LLM-modified files + // Get changeset that excludes already accepted/rejected files and only shows LLM-modified files const filteredChangeset = await checkpointFileChangeManager.getLLMOnlyChanges( task.taskId, task.fileContextTracker, ) - // Create changeset and send to webview (only LLM-modified, unaccepted files) + // Create changeset and send to webview (unaccepted files) const serializableChangeset = { baseCheckpoint: filteredChangeset.baseCheckpoint, files: filteredChangeset.files, @@ -274,13 +325,13 @@ async function checkGitInstallation( filesChanged: serializableChangeset, }) } else { - log(`[Task#checkpointCreated] No changes found between ${initialBaseline} and ${toHash}`) + log(`[Task#checkpointCreated] No changes found between ${currentBaseline} and ${toHash}`) } - // DON'T update the baseline - keep it at initial baseline for cumulative tracking + // DON'T update the baseline - keep it at current baseline for cumulative tracking // The baseline should only change when explicitly requested (e.g., checkpoint restore) log( - `[Task#checkpointCreated] Keeping FileChangeManager baseline at ${initialBaseline} for cumulative tracking`, + `[Task#checkpointCreated] Keeping FileChangeManager baseline at ${currentBaseline} for cumulative tracking`, ) } } catch (error) { @@ -457,12 +508,14 @@ export async function checkpointRestore( provider?.log(`[checkpointRestore] Cleared accept/reject state for fresh start`) } - // Calculate and send current changes (should be empty immediately after restore) - const changes = fileChangeManager.getChanges() - provider?.postMessageToWebview({ - type: "filesChanged", - filesChanged: changes.files.length > 0 ? changes : undefined, - }) + // Calculate and send current changes with LLM-only filtering (should be empty immediately after restore) + if (cline.taskId && cline.fileContextTracker) { + const changes = await fileChangeManager.getLLMOnlyChanges(cline.taskId, cline.fileContextTracker) + provider?.postMessageToWebview({ + type: "filesChanged", + filesChanged: changes.files.length > 0 ? changes : undefined, + }) + } } } catch (error) { provider?.log(`[checkpointRestore] Failed to update FileChangeManager baseline: ${error}`) diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts index 903e3c846e..6c2bb06cd5 100644 --- a/src/core/tools/applyDiffTool.ts +++ b/src/core/tools/applyDiffTool.ts @@ -230,6 +230,13 @@ export async function applyDiffToolLegacy( // Get the formatted response message const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) + // Track file as edited by LLM for FCO + try { + await cline.fileContextTracker.trackFileContext(relPath.toString(), "roo_edited") + } catch (error) { + console.error("Failed to track file edit in context:", error) + } + // Check for single SEARCH/REPLACE block warning const searchBlocks = (diffContent.match(/<<<<<<< SEARCH/g) || []).length const singleBlockNotice = diff --git a/src/core/tools/insertContentTool.ts b/src/core/tools/insertContentTool.ts index e22a368167..9784179949 100644 --- a/src/core/tools/insertContentTool.ts +++ b/src/core/tools/insertContentTool.ts @@ -174,9 +174,11 @@ export async function insertContentTool( await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) } - // Track file edit operation - if (relPath) { - await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource) + // Track file edit operation for FCO + try { + await cline.fileContextTracker.trackFileContext(relPath, "roo_edited") + } catch (error) { + console.error("Failed to track file edit in context:", error) } cline.didEditFile = true diff --git a/src/core/tools/searchAndReplaceTool.ts b/src/core/tools/searchAndReplaceTool.ts index 4912934415..5918e7a849 100644 --- a/src/core/tools/searchAndReplaceTool.ts +++ b/src/core/tools/searchAndReplaceTool.ts @@ -244,9 +244,11 @@ export async function searchAndReplaceTool( await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) } - // Track file edit operation - if (relPath) { - await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource) + // Track file edit operation for FCO + try { + await cline.fileContextTracker.trackFileContext(validRelPath.toString(), "roo_edited") + } catch (error) { + console.error("Failed to track file edit in context:", error) } cline.didEditFile = true diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index e82eab92bc..187236ed73 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -304,6 +304,13 @@ export async function writeToFileTool( // Get the formatted response message const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) + // Track file as edited by LLM for FCO + try { + await cline.fileContextTracker.trackFileContext(relPath.toString(), "roo_edited") + } catch (error) { + console.error("Failed to track file edit in context:", error) + } + pushToolResult(message) await cline.diffViewProvider.reset() diff --git a/webview-ui/src/components/file-changes/FilesChangedOverview.tsx b/webview-ui/src/components/file-changes/FilesChangedOverview.tsx index 3ec434cc10..2414cdc17b 100644 --- a/webview-ui/src/components/file-changes/FilesChangedOverview.tsx +++ b/webview-ui/src/components/file-changes/FilesChangedOverview.tsx @@ -419,6 +419,14 @@ const FileItem: React.FC = React.memo( }}> {file.uri} +
+ {t(`file-changes:file_types.${file.type}`)} +