Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/types/src/experiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const experimentIds = [
"preventFocusDisruption",
"imageGeneration",
"runSlashCommand",
"filesChangedOverview",
] as const

export const experimentIdsSchema = z.enum(experimentIds)
Expand All @@ -28,6 +29,7 @@ export const experimentsSchema = z.object({
preventFocusDisruption: z.boolean().optional(),
imageGeneration: z.boolean().optional(),
runSlashCommand: z.boolean().optional(),
filesChangedOverview: z.boolean().optional(),
})

export type Experiments = z.infer<typeof experimentsSchema>
Expand Down
21 changes: 21 additions & 0 deletions packages/types/src/file-changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type FileChangeType = "create" | "delete" | "edit"

export interface FileChange {
uri: string
type: FileChangeType
// Note: Checkpoint hashes are for backend use, but can be included
fromCheckpoint: string
toCheckpoint: string
// Line count information for display
linesAdded?: number
linesRemoved?: number
}

/**
* Represents the set of file changes for the webview.
* The `files` property is an array for easy serialization.
*/
export interface FileChangeset {
baseCheckpoint: string
files: FileChange[]
}
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export * from "./type-fu.js"
export * from "./vscode.js"

export * from "./providers/index.js"
export * from "./file-changes.js"
1 change: 1 addition & 0 deletions src/core/checkpoints/__tests__/checkpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe("Checkpoint functionality", () => {
getDiff: vi.fn().mockResolvedValue([]),
on: vi.fn(),
initShadowGit: vi.fn().mockResolvedValue(undefined),
getCurrentCheckpoint: vi.fn().mockReturnValue("base-hash"),
}

// Create mock provider
Expand Down
78 changes: 78 additions & 0 deletions src/core/checkpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getApiMetrics } from "../../shared/getApiMetrics"
import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider"

import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../services/checkpoints"
import { FileChangeManager } from "../../services/file-changes/FileChangeManager"

export async function getCheckpointService(
task: Task,
Expand Down Expand Up @@ -154,6 +155,63 @@ async function checkGitInstallation(
log("[Task#getCheckpointService] caught unexpected error in say('checkpoint_saved')")
console.error(err)
})
// Minimal FCO hook: compute and send Files Changed Overview on checkpoint
;(async () => {
try {
const fileChangeManager =
provider?.getFileChangeManager?.() ?? provider?.ensureFileChangeManager?.()
if (!fileChangeManager) return

let baseline = fileChangeManager.getChanges().baseCheckpoint
if (!baseline || baseline === "HEAD") {
baseline = service.baseHash || baseline || "HEAD"
}

const diffs = await service.getDiff({ from: baseline, to })
const stats = await service.getDiffStats({ from: baseline, to })
if (!diffs || diffs.length === 0) {
provider?.postMessageToWebview({ type: "filesChanged", filesChanged: undefined })
return
}

const files = diffs.map((change: any) => {
const before = change.content?.before ?? ""
const after = change.content?.after ?? ""
const type = !before && after ? "create" : before && !after ? "delete" : "edit"
const s = stats[change.paths.relative]
const lines = s
? { linesAdded: s.insertions, linesRemoved: s.deletions }
: FileChangeManager.calculateLineDifferences(before, after)
return {
uri: change.paths.relative,
type,
fromCheckpoint: baseline,
toCheckpoint: to,
linesAdded: lines.linesAdded,
linesRemoved: lines.linesRemoved,
}
})

const updated = await fileChangeManager.applyPerFileBaselines(files, service, to)
fileChangeManager.setFiles(updated)

if (task.taskId && task.fileContextTracker) {
const filtered = await fileChangeManager.getLLMOnlyChanges(
task.taskId,
task.fileContextTracker,
)
provider?.postMessageToWebview({
type: "filesChanged",
filesChanged: filtered.files.length > 0 ? filtered : undefined,
})
}
} catch (e) {
// Keep checkpoints functioning even if FCO hook fails
provider?.log?.(
`[Task#getCheckpointService] FCO update failed: ${e instanceof Error ? e.message : String(e)}`,
)
}
})()
} catch (err) {
log("[Task#getCheckpointService] caught unexpected error in on('checkpoint'), disabling checkpoints")
console.error(err)
Expand Down Expand Up @@ -225,6 +283,26 @@ export async function checkpointRestore(
TelemetryService.instance.captureCheckpointRestored(task.taskId)
await provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })

// Reset FileChangeManager baseline and clear states after restore
try {
const fileChangeManager = provider?.getFileChangeManager?.()
if (fileChangeManager) {
await fileChangeManager.updateBaseline(commitHash)
fileChangeManager.clearFileStates?.()
if (task.taskId && task.fileContextTracker) {
const filtered = await fileChangeManager.getLLMOnlyChanges(task.taskId, task.fileContextTracker)
provider?.postMessageToWebview({
type: "filesChanged",
filesChanged: filtered.files.length > 0 ? filtered : undefined,
})
}
}
} catch (e) {
provider?.log?.(
`[checkpointRestore] FCO baseline reset failed: ${e instanceof Error ? e.message : String(e)}`,
)
}

if (mode === "restore") {
await task.overwriteApiConversationHistory(task.apiConversationHistory.filter((m) => !m.ts || m.ts < ts))

Expand Down
7 changes: 7 additions & 0 deletions src/core/tools/applyDiffTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
15 changes: 15 additions & 0 deletions src/core/tools/attemptCompletionTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ export async function attemptCompletionTool(

cline.consecutiveMistakeCount = 0

// Create final checkpoint to capture the last file edit before completion
if (cline.enableCheckpoints) {
try {
await cline.checkpointSave(true) // Force save to capture any final changes
cline.providerRef
.deref()
?.log("[attemptCompletionTool] Created final checkpoint before task completion")
} catch (error) {
// Non-critical error, don't fail completion
cline.providerRef
.deref()
?.log(`[attemptCompletionTool] Failed to create final checkpoint: ${error}`)
}
}

// Command execution is permanently disabled in attempt_completion
// Users must use execute_command tool separately before attempt_completion
await cline.say("completion_result", result, undefined, false)
Expand Down
10 changes: 6 additions & 4 deletions src/core/tools/insertContentTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export async function insertContentTool(
cline.diffViewProvider.scrollToFirstDiff()
}

// Ask for approval (same for both flows)
// Ask for approval (same for both flows) - using askApproval wrapper to handle parameter ordering correctly
const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected)

if (!didApprove) {
Expand All @@ -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
Expand Down
8 changes: 5 additions & 3 deletions src/core/tools/searchAndReplaceTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 52 additions & 3 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { supportPrompt } from "../../shared/support-prompt"
import { GlobalFileNames } from "../../shared/globalFileNames"
import type { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage"
import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes"
import { experimentDefault } from "../../shared/experiments"
import { experimentDefault, EXPERIMENT_IDS } from "../../shared/experiments"
import { formatLanguage } from "../../shared/language"
import { WebviewMessage } from "../../shared/WebviewMessage"
import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels"
Expand Down Expand Up @@ -91,6 +91,8 @@ import { getSystemPromptFilePath } from "../prompts/sections/custom-system-promp
import { webviewMessageHandler } from "./webviewMessageHandler"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { FCOMessageHandler } from "../../services/file-changes/FCOMessageHandler"
import { FileChangeManager } from "../../services/file-changes/FileChangeManager"

/**
* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
Expand Down Expand Up @@ -134,6 +136,12 @@ export class ClineProvider
private taskCreationCallback: (task: Task) => void
private taskEventListeners: WeakMap<Task, Array<() => void>> = new WeakMap()
private currentWorkspacePath: string | undefined
// FileChangeManager instances scoped per taskId
private fileChangeManagers: Map<string, any> = new Map()
// Track the last committed checkpoint hash per task for FCO delta updates
private lastCheckpointByTaskId: Map<string, string> = new Map()
// FCO message handler for universal baseline management
private fcoMessageHandler: FCOMessageHandler

private recentTasksCache?: string[]
private pendingOperations: Map<string, PendingEditOperation> = new Map()
Expand Down Expand Up @@ -175,6 +183,9 @@ export class ClineProvider
await this.postStateToWebview()
})

// Initialize FCO message handler for universal baseline management
this.fcoMessageHandler = new FCOMessageHandler(this)

// Initialize MCP Hub through the singleton manager
McpServerManager.getInstance(this.context, this)
.then((hub) => {
Expand Down Expand Up @@ -1119,8 +1130,14 @@ export class ClineProvider
* @param webview A reference to the extension webview
*/
private setWebviewMessageListener(webview: vscode.Webview) {
const onReceiveMessage = async (message: WebviewMessage) =>
webviewMessageHandler(this, message, this.marketplaceManager)
const onReceiveMessage = async (message: WebviewMessage) => {
// Route Files Changed Overview messages first
if (this.fcoMessageHandler.shouldHandleMessage(message)) {
await this.fcoMessageHandler.handleMessage(message)
return
}
await webviewMessageHandler(this, message, this.marketplaceManager)
}

const messageDisposable = webview.onDidReceiveMessage(onReceiveMessage)
this.webviewDisposables.push(messageDisposable)
Expand Down Expand Up @@ -2154,6 +2171,38 @@ export class ClineProvider
return this.contextProxy.getValue(key)
}

// File Change Manager methods
public getFileChangeManager(): any {
const task = this.getCurrentTask()
if (!task) return undefined
return this.fileChangeManagers.get(task.taskId)
}

public ensureFileChangeManager(): any {
const task = this.getCurrentTask()
if (!task) return undefined
const existing = this.fileChangeManagers.get(task.taskId)
if (existing) return existing
// Default baseline to HEAD until checkpoints initialize and update it
const manager = new FileChangeManager("HEAD")
this.fileChangeManagers.set(task.taskId, manager)
return manager
}

// FCO Message Handler access
public getFCOMessageHandler(): FCOMessageHandler {
return this.fcoMessageHandler
}

// Track last checkpoint per task for delta-based FCO updates
public setLastCheckpointForTask(taskId: string, commitHash: string) {
this.lastCheckpointByTaskId.set(taskId, commitHash)
}

public getLastCheckpointForTask(taskId: string): string | undefined {
return this.lastCheckpointByTaskId.get(taskId)
}

public async setValue<K extends keyof RooCodeSettings>(key: K, value: RooCodeSettings[K]) {
await this.contextProxy.setValue(key, value)
}
Expand Down
Loading
Loading