mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
- Remove deprecated BridgeOrchestrator imports and usage from Task.ts, extension.ts - Replace removed getUserSettings() method calls with fallbacks - Fix type compatibility issues with clineMessages parameter - Fix FileChangeManager baseline assignment and rejection logic - Auto-assign fromCheckpoint as initial baseline when files enter FCO - Fix rejection to preserve existing baselines - Fix acceptance to properly update baselines to current checkpoint - Add missing mock setups in tests for applyPerFileBaselines calls - Update test expectations to match calculated line differences - All TypeScript type checking now passes (11/11 packages)
318 lines
9.9 KiB
TypeScript
318 lines
9.9 KiB
TypeScript
import { FileChange, FileChangeset, FileChangeType } from "@roo-code/types"
|
|
import type { FileContextTracker } from "../../core/context-tracking/FileContextTracker"
|
|
|
|
/**
|
|
* Simplified FileChangeManager - Pure diff calculation service
|
|
* No complex persistence, events, or tool integration
|
|
*/
|
|
export class FileChangeManager {
|
|
private changeset: FileChangeset
|
|
private acceptedBaselines: Map<string, string> // uri -> baseline checkpoint (for both accept and reject)
|
|
|
|
constructor(baseCheckpoint: string) {
|
|
this.changeset = {
|
|
baseCheckpoint,
|
|
files: [],
|
|
}
|
|
this.acceptedBaselines = new Map()
|
|
}
|
|
|
|
/**
|
|
* Get current changeset - visibility determined by actual diffs
|
|
*/
|
|
public getChanges(): FileChangeset {
|
|
// Filter files based on baseline diff - show only if different from baseline
|
|
const filteredFiles = this.changeset.files.filter((file) => {
|
|
const baseline = this.acceptedBaselines.get(file.uri)
|
|
if (!baseline) {
|
|
// No baseline set, always show
|
|
return true
|
|
}
|
|
// Only show if file has changed from its baseline
|
|
return file.toCheckpoint !== baseline
|
|
})
|
|
|
|
return {
|
|
...this.changeset,
|
|
files: filteredFiles,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get changeset filtered to only show LLM-modified files
|
|
*/
|
|
public async getLLMOnlyChanges(taskId: string, fileContextTracker: FileContextTracker): Promise<FileChangeset> {
|
|
// Get task metadata to determine which files were modified by LLM
|
|
const taskMetadata = await fileContextTracker.getTaskMetadata(taskId)
|
|
|
|
// Get files that were modified by LLM (record_source: "roo_edited")
|
|
const llmModifiedFiles = new Set(
|
|
taskMetadata.files_in_context
|
|
.filter((entry) => entry.record_source === "roo_edited")
|
|
.map((entry) => entry.path),
|
|
)
|
|
|
|
// Filter changeset to only include LLM-modified files that haven't been accepted
|
|
const filteredFiles = this.changeset.files.filter((file) => {
|
|
if (!llmModifiedFiles.has(file.uri)) {
|
|
return false
|
|
}
|
|
const baseline = this.acceptedBaselines.get(file.uri)
|
|
// File is "not accepted" if baseline equals fromCheckpoint (initial baseline)
|
|
// File is "accepted" if baseline equals toCheckpoint (updated baseline)
|
|
return baseline === file.fromCheckpoint
|
|
})
|
|
|
|
return {
|
|
...this.changeset,
|
|
files: filteredFiles,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a specific file change
|
|
*/
|
|
public getFileChange(uri: string): FileChange | undefined {
|
|
return this.changeset.files.find((file) => file.uri === uri)
|
|
}
|
|
|
|
/**
|
|
* Accept a specific file change
|
|
*/
|
|
public async acceptChange(uri: string): Promise<void> {
|
|
const file = this.getFileChange(uri)
|
|
if (file) {
|
|
// Set baseline to current checkpoint - file will disappear from FCO naturally (no diff from baseline)
|
|
this.acceptedBaselines.set(uri, file.toCheckpoint)
|
|
}
|
|
// If file doesn't exist (was rejected), we can't accept it without current state info
|
|
// This scenario might indicate test logic issue or need for different handling
|
|
}
|
|
|
|
/**
|
|
* Reject a specific file change
|
|
*/
|
|
public async rejectChange(uri: string): Promise<void> {
|
|
// Remove the file from changeset - it will be reverted externally
|
|
// If file is edited again after reversion, it will reappear via updateFCOAfterEdit
|
|
this.changeset.files = this.changeset.files.filter((file) => file.uri !== uri)
|
|
}
|
|
|
|
/**
|
|
* Accept all file changes - updates global baseline and clears FCO
|
|
*/
|
|
public async acceptAll(): Promise<void> {
|
|
if (this.changeset.files.length > 0) {
|
|
// Get the latest checkpoint from any file (should all be the same)
|
|
const currentCheckpoint = this.changeset.files[0].toCheckpoint
|
|
// Update global baseline to current checkpoint
|
|
this.changeset.baseCheckpoint = currentCheckpoint
|
|
}
|
|
// Clear all files and per-file baselines since we have new global baseline
|
|
this.changeset.files = []
|
|
this.acceptedBaselines.clear()
|
|
}
|
|
|
|
/**
|
|
* Reject all file changes
|
|
*/
|
|
public async rejectAll(): Promise<void> {
|
|
// Clear all files from changeset - they will be reverted externally
|
|
// If files are edited again after reversion, they will reappear via updateFCOAfterEdit
|
|
this.changeset.files = []
|
|
}
|
|
|
|
/**
|
|
* Update the baseline checkpoint and recalculate changes
|
|
*/
|
|
public async updateBaseline(
|
|
newBaselineCheckpoint: string,
|
|
_getDiff?: (from: string, to: string) => Promise<{ filePath: string; content: string }[]>,
|
|
_checkpointService?: {
|
|
checkpoints: string[]
|
|
baseHash?: string
|
|
},
|
|
): Promise<void> {
|
|
this.changeset.baseCheckpoint = newBaselineCheckpoint
|
|
|
|
// Simple approach: request fresh calculation from backend
|
|
// The actual diff calculation should be handled by the checkpoint service
|
|
this.changeset.files = []
|
|
|
|
// Clear accepted baselines - baseline change means we're starting fresh
|
|
// This happens during checkpoint restore (time travel) where we want a clean slate
|
|
this.acceptedBaselines.clear()
|
|
}
|
|
|
|
/**
|
|
* Set the files for the changeset (called by backend when files change)
|
|
* Preserves existing accept/reject state for files with the same URI
|
|
*/
|
|
public setFiles(files: FileChange[]): void {
|
|
files.forEach((file) => {
|
|
// For new files (not yet in changeset), assign initial baseline
|
|
if (!this.acceptedBaselines.has(file.uri)) {
|
|
// Use fromCheckpoint as initial baseline (the state file started from)
|
|
this.acceptedBaselines.set(file.uri, file.fromCheckpoint)
|
|
}
|
|
})
|
|
this.changeset.files = files
|
|
}
|
|
|
|
/**
|
|
* Clear accepted baselines (called when new checkpoint created)
|
|
*/
|
|
public clearFileStates(): void {
|
|
this.acceptedBaselines.clear()
|
|
}
|
|
|
|
/**
|
|
* Apply per-file baselines to a changeset for incremental diff calculation
|
|
* For files that have been accepted, calculate diff from their acceptance point instead of global baseline
|
|
*/
|
|
public async applyPerFileBaselines(
|
|
baseChanges: FileChange[],
|
|
checkpointService: any,
|
|
currentCheckpoint: string,
|
|
): Promise<FileChange[]> {
|
|
const updatedChanges: FileChange[] = []
|
|
|
|
for (const change of baseChanges) {
|
|
// Get accepted baseline for this file (null = use global baseline)
|
|
const acceptedBaseline = this.acceptedBaselines.get(change.uri)
|
|
|
|
if (acceptedBaseline) {
|
|
// This file was accepted before - calculate incremental diff from acceptance point
|
|
try {
|
|
const incrementalChanges = await checkpointService.getDiff({
|
|
from: acceptedBaseline,
|
|
to: currentCheckpoint,
|
|
})
|
|
|
|
// Find this specific file in the incremental diff
|
|
const incrementalChange = incrementalChanges?.find((c: any) => c.paths.relative === change.uri)
|
|
|
|
if (incrementalChange) {
|
|
// Convert to FileChange with per-file baseline
|
|
const type = (
|
|
incrementalChange.paths.newFile
|
|
? "create"
|
|
: incrementalChange.paths.deletedFile
|
|
? "delete"
|
|
: "edit"
|
|
) as FileChangeType
|
|
|
|
let linesAdded = 0
|
|
let linesRemoved = 0
|
|
|
|
if (type === "create") {
|
|
linesAdded = incrementalChange.content.after
|
|
? incrementalChange.content.after.split("\n").length
|
|
: 0
|
|
linesRemoved = 0
|
|
} else if (type === "delete") {
|
|
linesAdded = 0
|
|
linesRemoved = incrementalChange.content.before
|
|
? incrementalChange.content.before.split("\n").length
|
|
: 0
|
|
} else {
|
|
const lineDifferences = FileChangeManager.calculateLineDifferences(
|
|
incrementalChange.content.before || "",
|
|
incrementalChange.content.after || "",
|
|
)
|
|
linesAdded = lineDifferences.linesAdded
|
|
linesRemoved = lineDifferences.linesRemoved
|
|
}
|
|
|
|
updatedChanges.push({
|
|
uri: change.uri,
|
|
type,
|
|
fromCheckpoint: acceptedBaseline, // Use per-file baseline
|
|
toCheckpoint: currentCheckpoint,
|
|
linesAdded,
|
|
linesRemoved,
|
|
})
|
|
}
|
|
// If no incremental change found, file hasn't changed since acceptance - don't include it
|
|
} catch (error) {
|
|
// If we can't calculate incremental diff, fall back to original change
|
|
updatedChanges.push(change)
|
|
}
|
|
} else {
|
|
// File was never accepted - use original change
|
|
updatedChanges.push(change)
|
|
}
|
|
}
|
|
|
|
return updatedChanges
|
|
}
|
|
|
|
/**
|
|
* Calculate line differences between two file contents
|
|
* Uses a simple line-by-line comparison to count actual changes
|
|
*/
|
|
public static calculateLineDifferences(
|
|
originalContent: string,
|
|
newContent: string,
|
|
): { linesAdded: number; linesRemoved: number } {
|
|
const originalLines = originalContent === "" ? [] : originalContent.split("\n")
|
|
const newLines = newContent === "" ? [] : newContent.split("\n")
|
|
|
|
// For proper diff calculation, we need to compare line by line
|
|
// This is a simplified approach that works well for most cases
|
|
|
|
const maxLines = Math.max(originalLines.length, newLines.length)
|
|
let linesAdded = 0
|
|
let linesRemoved = 0
|
|
|
|
// Compare each line position
|
|
for (let i = 0; i < maxLines; i++) {
|
|
const originalLine = i < originalLines.length ? originalLines[i] : undefined
|
|
const newLine = i < newLines.length ? newLines[i] : undefined
|
|
|
|
if (originalLine === undefined && newLine !== undefined) {
|
|
// Line was added
|
|
linesAdded++
|
|
} else if (originalLine !== undefined && newLine === undefined) {
|
|
// Line was removed
|
|
linesRemoved++
|
|
} else if (originalLine !== newLine) {
|
|
// Line was modified (count as both removed and added)
|
|
linesRemoved++
|
|
linesAdded++
|
|
}
|
|
// If lines are identical, no change
|
|
}
|
|
|
|
return { linesAdded, linesRemoved }
|
|
}
|
|
|
|
/**
|
|
* Dispose of the manager (for compatibility)
|
|
*/
|
|
public dispose(): void {
|
|
this.changeset.files = []
|
|
this.acceptedBaselines.clear()
|
|
}
|
|
}
|
|
|
|
// Export the error types for backward compatibility
|
|
export enum FileChangeErrorType {
|
|
PERSISTENCE_FAILED = "PERSISTENCE_FAILED",
|
|
FILE_NOT_FOUND = "FILE_NOT_FOUND",
|
|
PERMISSION_DENIED = "PERMISSION_DENIED",
|
|
DISK_FULL = "DISK_FULL",
|
|
GENERIC_ERROR = "GENERIC_ERROR",
|
|
}
|
|
|
|
export class FileChangeError extends Error {
|
|
constructor(
|
|
public type: FileChangeErrorType,
|
|
public uri?: string,
|
|
message?: string,
|
|
public originalError?: Error,
|
|
) {
|
|
super(message || originalError?.message || "File change operation failed")
|
|
this.name = "FileChangeError"
|
|
}
|
|
}
|