Skip to content

Commit aaa4fed

Browse files
committed
Files changed overview, No checkpoint changes.
- Add FilesChangedOverview UI + i18n and supporting hook (useDebouncedAction) - Introduce FileChangeManager and FCOMessageHandler services - Wire FCO into ClineProvider and webviewMessageHandler (experiment toggle) - Track LLM-edited files in tools (applyDiff, insertContent, searchAndReplace) - Add ShadowCheckpointService helpers: getContent and getCurrentCheckpoint - Minimal checkpoint integration: send filesChanged on checkpoint; reset baseline on restore - No checkpoint behavior changes; scoped to upstream/main - removed UseDebouncedAction since it is already implemented elsewhere. - added changes mentioned by roo in the review
1 parent 39030bf commit aaa4fed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+4579
-11
lines changed

packages/types/src/experiment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const experimentIds = [
1212
"preventFocusDisruption",
1313
"imageGeneration",
1414
"runSlashCommand",
15+
"filesChangedOverview",
1516
] as const
1617

1718
export const experimentIdsSchema = z.enum(experimentIds)
@@ -28,6 +29,7 @@ export const experimentsSchema = z.object({
2829
preventFocusDisruption: z.boolean().optional(),
2930
imageGeneration: z.boolean().optional(),
3031
runSlashCommand: z.boolean().optional(),
32+
filesChangedOverview: z.boolean().optional(),
3133
})
3234

3335
export type Experiments = z.infer<typeof experimentsSchema>

packages/types/src/file-changes.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export type FileChangeType = "create" | "delete" | "edit"
2+
3+
export interface FileChange {
4+
uri: string
5+
type: FileChangeType
6+
// Note: Checkpoint hashes are for backend use, but can be included
7+
fromCheckpoint: string
8+
toCheckpoint: string
9+
// Line count information for display
10+
linesAdded?: number
11+
linesRemoved?: number
12+
}
13+
14+
/**
15+
* Represents the set of file changes for the webview.
16+
* The `files` property is an array for easy serialization.
17+
*/
18+
export interface FileChangeset {
19+
baseCheckpoint: string
20+
files: FileChange[]
21+
}

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ export * from "./type-fu.js"
2323
export * from "./vscode.js"
2424

2525
export * from "./providers/index.js"
26+
export * from "./file-changes.js"

src/core/checkpoints/__tests__/checkpoint.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe("Checkpoint functionality", () => {
5151
getDiff: vi.fn().mockResolvedValue([]),
5252
on: vi.fn(),
5353
initShadowGit: vi.fn().mockResolvedValue(undefined),
54+
getCurrentCheckpoint: vi.fn().mockReturnValue("base-hash"),
5455
}
5556

5657
// Create mock provider

src/core/checkpoints/index.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getApiMetrics } from "../../shared/getApiMetrics"
1515
import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider"
1616

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

1920
export async function getCheckpointService(
2021
task: Task,
@@ -154,6 +155,63 @@ async function checkGitInstallation(
154155
log("[Task#getCheckpointService] caught unexpected error in say('checkpoint_saved')")
155156
console.error(err)
156157
})
158+
// Minimal FCO hook: compute and send Files Changed Overview on checkpoint
159+
;(async () => {
160+
try {
161+
const fileChangeManager =
162+
provider?.getFileChangeManager?.() ?? provider?.ensureFileChangeManager?.()
163+
if (!fileChangeManager) return
164+
165+
let baseline = fileChangeManager.getChanges().baseCheckpoint
166+
if (!baseline || baseline === "HEAD") {
167+
baseline = service.baseHash || baseline || "HEAD"
168+
}
169+
170+
const diffs = await service.getDiff({ from: baseline, to })
171+
const stats = await service.getDiffStats({ from: baseline, to })
172+
if (!diffs || diffs.length === 0) {
173+
provider?.postMessageToWebview({ type: "filesChanged", filesChanged: undefined })
174+
return
175+
}
176+
177+
const files = diffs.map((change: any) => {
178+
const before = change.content?.before ?? ""
179+
const after = change.content?.after ?? ""
180+
const type = !before && after ? "create" : before && !after ? "delete" : "edit"
181+
const s = stats[change.paths.relative]
182+
const lines = s
183+
? { linesAdded: s.insertions, linesRemoved: s.deletions }
184+
: FileChangeManager.calculateLineDifferences(before, after)
185+
return {
186+
uri: change.paths.relative,
187+
type,
188+
fromCheckpoint: baseline,
189+
toCheckpoint: to,
190+
linesAdded: lines.linesAdded,
191+
linesRemoved: lines.linesRemoved,
192+
}
193+
})
194+
195+
const updated = await fileChangeManager.applyPerFileBaselines(files, service, to)
196+
fileChangeManager.setFiles(updated)
197+
198+
if (task.taskId && task.fileContextTracker) {
199+
const filtered = await fileChangeManager.getLLMOnlyChanges(
200+
task.taskId,
201+
task.fileContextTracker,
202+
)
203+
provider?.postMessageToWebview({
204+
type: "filesChanged",
205+
filesChanged: filtered.files.length > 0 ? filtered : undefined,
206+
})
207+
}
208+
} catch (e) {
209+
// Keep checkpoints functioning even if FCO hook fails
210+
provider?.log?.(
211+
`[Task#getCheckpointService] FCO update failed: ${e instanceof Error ? e.message : String(e)}`,
212+
)
213+
}
214+
})()
157215
} catch (err) {
158216
log("[Task#getCheckpointService] caught unexpected error in on('checkpoint'), disabling checkpoints")
159217
console.error(err)
@@ -225,6 +283,26 @@ export async function checkpointRestore(
225283
TelemetryService.instance.captureCheckpointRestored(task.taskId)
226284
await provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
227285

286+
// Reset FileChangeManager baseline and clear states after restore
287+
try {
288+
const fileChangeManager = provider?.getFileChangeManager?.()
289+
if (fileChangeManager) {
290+
await fileChangeManager.updateBaseline(commitHash)
291+
fileChangeManager.clearFileStates?.()
292+
if (task.taskId && task.fileContextTracker) {
293+
const filtered = await fileChangeManager.getLLMOnlyChanges(task.taskId, task.fileContextTracker)
294+
provider?.postMessageToWebview({
295+
type: "filesChanged",
296+
filesChanged: filtered.files.length > 0 ? filtered : undefined,
297+
})
298+
}
299+
}
300+
} catch (e) {
301+
provider?.log?.(
302+
`[checkpointRestore] FCO baseline reset failed: ${e instanceof Error ? e.message : String(e)}`,
303+
)
304+
}
305+
228306
if (mode === "restore") {
229307
await task.overwriteApiConversationHistory(task.apiConversationHistory.filter((m) => !m.ts || m.ts < ts))
230308

src/core/tools/applyDiffTool.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ export async function applyDiffToolLegacy(
230230
// Get the formatted response message
231231
const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists)
232232

233+
// Track file as edited by LLM for FCO
234+
try {
235+
await cline.fileContextTracker.trackFileContext(relPath.toString(), "roo_edited")
236+
} catch (error) {
237+
console.error("Failed to track file edit in context:", error)
238+
}
239+
233240
// Check for single SEARCH/REPLACE block warning
234241
const searchBlocks = (diffContent.match(/<<<<<<< SEARCH/g) || []).length
235242
const singleBlockNotice =

src/core/tools/attemptCompletionTool.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ export async function attemptCompletionTool(
8989

9090
cline.consecutiveMistakeCount = 0
9191

92+
// Create final checkpoint to capture the last file edit before completion
93+
if (cline.enableCheckpoints) {
94+
try {
95+
await cline.checkpointSave(true) // Force save to capture any final changes
96+
cline.providerRef
97+
.deref()
98+
?.log("[attemptCompletionTool] Created final checkpoint before task completion")
99+
} catch (error) {
100+
// Non-critical error, don't fail completion
101+
cline.providerRef
102+
.deref()
103+
?.log(`[attemptCompletionTool] Failed to create final checkpoint: ${error}`)
104+
}
105+
}
106+
92107
// Command execution is permanently disabled in attempt_completion
93108
// Users must use execute_command tool separately before attempt_completion
94109
await cline.say("completion_result", result, undefined, false)

src/core/tools/insertContentTool.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export async function insertContentTool(
152152
cline.diffViewProvider.scrollToFirstDiff()
153153
}
154154

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

158158
if (!didApprove) {
@@ -174,9 +174,11 @@ export async function insertContentTool(
174174
await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
175175
}
176176

177-
// Track file edit operation
178-
if (relPath) {
179-
await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource)
177+
// Track file edit operation for FCO
178+
try {
179+
await cline.fileContextTracker.trackFileContext(relPath, "roo_edited")
180+
} catch (error) {
181+
console.error("Failed to track file edit in context:", error)
180182
}
181183

182184
cline.didEditFile = true

src/core/tools/searchAndReplaceTool.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,11 @@ export async function searchAndReplaceTool(
244244
await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
245245
}
246246

247-
// Track file edit operation
248-
if (relPath) {
249-
await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource)
247+
// Track file edit operation for FCO
248+
try {
249+
await cline.fileContextTracker.trackFileContext(validRelPath.toString(), "roo_edited")
250+
} catch (error) {
251+
console.error("Failed to track file edit in context:", error)
250252
}
251253

252254
cline.didEditFile = true

src/core/webview/ClineProvider.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import { supportPrompt } from "../../shared/support-prompt"
5050
import { GlobalFileNames } from "../../shared/globalFileNames"
5151
import type { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage"
5252
import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes"
53-
import { experimentDefault } from "../../shared/experiments"
53+
import { experimentDefault, EXPERIMENT_IDS } from "../../shared/experiments"
5454
import { formatLanguage } from "../../shared/language"
5555
import { WebviewMessage } from "../../shared/WebviewMessage"
5656
import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels"
@@ -91,6 +91,8 @@ import { getSystemPromptFilePath } from "../prompts/sections/custom-system-promp
9191
import { webviewMessageHandler } from "./webviewMessageHandler"
9292
import { getNonce } from "./getNonce"
9393
import { getUri } from "./getUri"
94+
import { FCOMessageHandler } from "../../services/file-changes/FCOMessageHandler"
95+
import { FileChangeManager } from "../../services/file-changes/FileChangeManager"
9496

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

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

186+
// Initialize FCO message handler for universal baseline management
187+
this.fcoMessageHandler = new FCOMessageHandler(this)
188+
178189
// Initialize MCP Hub through the singleton manager
179190
McpServerManager.getInstance(this.context, this)
180191
.then((hub) => {
@@ -1119,8 +1130,14 @@ export class ClineProvider
11191130
* @param webview A reference to the extension webview
11201131
*/
11211132
private setWebviewMessageListener(webview: vscode.Webview) {
1122-
const onReceiveMessage = async (message: WebviewMessage) =>
1123-
webviewMessageHandler(this, message, this.marketplaceManager)
1133+
const onReceiveMessage = async (message: WebviewMessage) => {
1134+
// Route Files Changed Overview messages first
1135+
if (this.fcoMessageHandler.shouldHandleMessage(message)) {
1136+
await this.fcoMessageHandler.handleMessage(message)
1137+
return
1138+
}
1139+
await webviewMessageHandler(this, message, this.marketplaceManager)
1140+
}
11241141

11251142
const messageDisposable = webview.onDidReceiveMessage(onReceiveMessage)
11261143
this.webviewDisposables.push(messageDisposable)
@@ -2154,6 +2171,38 @@ export class ClineProvider
21542171
return this.contextProxy.getValue(key)
21552172
}
21562173

2174+
// File Change Manager methods
2175+
public getFileChangeManager(): any {
2176+
const task = this.getCurrentTask()
2177+
if (!task) return undefined
2178+
return this.fileChangeManagers.get(task.taskId)
2179+
}
2180+
2181+
public ensureFileChangeManager(): any {
2182+
const task = this.getCurrentTask()
2183+
if (!task) return undefined
2184+
const existing = this.fileChangeManagers.get(task.taskId)
2185+
if (existing) return existing
2186+
// Default baseline to HEAD until checkpoints initialize and update it
2187+
const manager = new FileChangeManager("HEAD")
2188+
this.fileChangeManagers.set(task.taskId, manager)
2189+
return manager
2190+
}
2191+
2192+
// FCO Message Handler access
2193+
public getFCOMessageHandler(): FCOMessageHandler {
2194+
return this.fcoMessageHandler
2195+
}
2196+
2197+
// Track last checkpoint per task for delta-based FCO updates
2198+
public setLastCheckpointForTask(taskId: string, commitHash: string) {
2199+
this.lastCheckpointByTaskId.set(taskId, commitHash)
2200+
}
2201+
2202+
public getLastCheckpointForTask(taskId: string): string | undefined {
2203+
return this.lastCheckpointByTaskId.get(taskId)
2204+
}
2205+
21572206
public async setValue<K extends keyof RooCodeSettings>(key: K, value: RooCodeSettings[K]) {
21582207
await this.contextProxy.setValue(key, value)
21592208
}

0 commit comments

Comments
 (0)