Skip to content

Commit 65cb66b

Browse files
committed
Brought FCO to head in working order.
update checkpoint test verbiage and don't suppress messages in testing Pulled all I could from fco clean and got it into a working state. The changes implement a Files Changed Overview (FCO) feature that tracks and displays file modifications made by │ │ │ │ the AI assistant during task execution. This is a major feature addition with: │ │ │ │ │ │ │ │ - New experiment flag: filesChangedOverview to control the feature │ │ │ │ - Core service layer: FileChangeManager and FCOMessageHandler for managing file change state │ │ │ │ - Checkpoint integration: Enhanced checkpoint system to work with FCO for diff calculations │ │ │ │ - UI components: React-based FilesChangedOverview component with virtualization │ │ │ │ - Message system: New message types for FCO communication between webview and backend │ │ │ │ - Tool integration: All file editing tools now track changes for FCO │ │ │ │ - Type definitions: New TypeScript types for FileChange, FileChangeset, etc. │ │ │ │ - Internationalization: Translation files added for all supported languages final changes for fco fixing type checks after rebase
1 parent 2571781 commit 65cb66b

File tree

70 files changed

+6945
-131
lines changed

Some content is hidden

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

70 files changed

+6945
-131
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/assistant-message/presentAssistantMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { Task } from "../task/Task"
3737
import { codebaseSearchTool } from "../tools/codebaseSearchTool"
3838
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
3939
import { applyDiffToolLegacy } from "../tools/applyDiffTool"
40+
// Live FCO updates removed: FCO now updates on checkpoint events only
4041

4142
/**
4243
* Processes and presents assistant message content to the user interface.

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

Lines changed: 3 additions & 2 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
@@ -111,7 +112,7 @@ describe("Checkpoint functionality", () => {
111112
// saveCheckpoint should have been called
112113
expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledWith(
113114
expect.stringContaining("Task: test-task-id"),
114-
{ allowEmpty: true, suppressMessage: false },
115+
{ allowEmpty: true, files: undefined },
115116
)
116117

117118
// Result should contain the commit hash
@@ -329,7 +330,7 @@ describe("Checkpoint functionality", () => {
329330
})
330331
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
331332
"vscode.changes",
332-
"Changes compare with next checkpoint",
333+
"Changes since previous checkpoint",
333334
expect.any(Array),
334335
)
335336
})
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { vitest } from "vitest"
2+
3+
export const createMockTask = (options: {
4+
taskId: string
5+
hasExistingCheckpoints?: boolean
6+
enableCheckpoints?: boolean
7+
provider?: any
8+
}) => {
9+
const mockTask = {
10+
taskId: options.taskId,
11+
instanceId: "test-instance",
12+
rootTask: undefined as any,
13+
parentTask: undefined as any,
14+
taskNumber: 1,
15+
workspacePath: "/mock/workspace",
16+
enableCheckpoints: options.enableCheckpoints ?? true,
17+
checkpointService: null as any,
18+
checkpointServiceInitializing: false,
19+
ongoingCheckpointSaves: new Map(),
20+
clineMessages: options.hasExistingCheckpoints
21+
? [{ say: "checkpoint_saved", ts: Date.now(), text: "existing-checkpoint-hash" }]
22+
: [],
23+
providerRef: {
24+
deref: () => options.provider || createMockProvider(),
25+
},
26+
fileContextTracker: {},
27+
todoList: undefined,
28+
}
29+
30+
return mockTask
31+
}
32+
33+
export const createMockProvider = () => ({
34+
getFileChangeManager: vitest.fn(),
35+
ensureFileChangeManager: vitest.fn(),
36+
log: vitest.fn(),
37+
postMessageToWebview: vitest.fn(),
38+
getGlobalState: vitest.fn(),
39+
})
40+
41+
// Mock checkpoint service for testing
42+
export const createMockCheckpointService = () => ({
43+
saveCheckpoint: vitest.fn().mockResolvedValue({
44+
commit: "mock-checkpoint-hash",
45+
message: "Mock checkpoint",
46+
}),
47+
restoreCheckpoint: vitest.fn().mockResolvedValue(true),
48+
getDiff: vitest.fn().mockResolvedValue([]),
49+
getCheckpoints: vitest.fn().mockReturnValue([]),
50+
getCurrentCheckpoint: vitest.fn().mockReturnValue("mock-current-checkpoint"),
51+
initShadowGit: vitest.fn().mockResolvedValue(true),
52+
baseHash: "mock-base-hash",
53+
})
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
// Use doMock to apply the mock dynamically
2+
vitest.doMock("../../utils/path", () => ({
3+
getWorkspacePath: vitest.fn(() => {
4+
console.log("getWorkspacePath mock called, returning:", "/mock/workspace")
5+
return "/mock/workspace"
6+
}),
7+
}))
8+
9+
// Mock the RepoPerTaskCheckpointService
10+
vitest.mock("../../../services/checkpoints", () => ({
11+
RepoPerTaskCheckpointService: {
12+
create: vitest.fn(),
13+
},
14+
}))
15+
16+
// Mock the TelemetryService to prevent unhandled rejections
17+
vitest.mock("@roo-code/telemetry", () => ({
18+
TelemetryService: {
19+
instance: {
20+
captureCheckpointCreated: vitest.fn(),
21+
captureCheckpointRestored: vitest.fn(),
22+
captureCheckpointDiffed: vitest.fn(),
23+
},
24+
},
25+
}))
26+
27+
import { describe, it, expect, beforeEach, afterEach, vitest } from "vitest"
28+
import * as path from "path"
29+
import * as fs from "fs/promises"
30+
import * as os from "os"
31+
import { EventEmitter } from "events"
32+
33+
// Import these modules after mocks are set up
34+
let getCheckpointService: any
35+
let RepoPerTaskCheckpointService: any
36+
37+
// Set up the imports after mocks
38+
beforeAll(async () => {
39+
const checkpointsModule = await import("../index")
40+
const checkpointServiceModule = await import("../../../services/checkpoints")
41+
getCheckpointService = checkpointsModule.getCheckpointService
42+
RepoPerTaskCheckpointService = checkpointServiceModule.RepoPerTaskCheckpointService
43+
})
44+
45+
// Mock the FileChangeManager to avoid complex dependencies
46+
const mockFileChangeManager = {
47+
_baseline: "HEAD" as string,
48+
getChanges: vitest.fn(),
49+
updateBaseline: vitest.fn(),
50+
setFiles: vitest.fn(),
51+
getLLMOnlyChanges: vitest.fn(),
52+
}
53+
54+
// Create a temporary directory for mock global storage
55+
let mockGlobalStorageDir: string
56+
57+
// Mock the provider
58+
const mockProvider = {
59+
getFileChangeManager: vitest.fn(() => mockFileChangeManager),
60+
log: vitest.fn(),
61+
get context() {
62+
return {
63+
globalStorageUri: {
64+
fsPath: mockGlobalStorageDir,
65+
},
66+
}
67+
},
68+
}
69+
70+
// Mock the Task object with proper typing
71+
const createMockTask = (options: { taskId: string; hasExistingCheckpoints: boolean; enableCheckpoints?: boolean }) => {
72+
const mockTask = {
73+
taskId: options.taskId,
74+
instanceId: "test-instance",
75+
rootTask: undefined as any,
76+
parentTask: undefined as any,
77+
taskNumber: 1,
78+
workspacePath: "/mock/workspace",
79+
enableCheckpoints: options.enableCheckpoints ?? true,
80+
checkpointService: null as any,
81+
checkpointServiceInitializing: false,
82+
ongoingCheckpointSaves: new Map(),
83+
clineMessages: options.hasExistingCheckpoints
84+
? [{ say: "checkpoint_saved", ts: Date.now(), text: "existing-checkpoint-hash" }]
85+
: [],
86+
providerRef: {
87+
deref: () => mockProvider,
88+
},
89+
fileContextTracker: {},
90+
// Add minimal required properties to satisfy Task interface
91+
todoList: undefined,
92+
userMessageContent: "",
93+
apiConversationHistory: [],
94+
customInstructions: "",
95+
alwaysAllowReadOnly: false,
96+
alwaysAllowWrite: false,
97+
alwaysAllowExecute: false,
98+
alwaysAllowBrowser: false,
99+
alwaysAllowMcp: false,
100+
createdAt: Date.now(),
101+
historyErrors: [],
102+
askResponse: undefined,
103+
askResponseText: "",
104+
abort: vitest.fn(),
105+
isAborting: false,
106+
} as any // Cast to any to avoid needing to implement all Task methods
107+
return mockTask
108+
}
109+
110+
describe("getCheckpointService orchestration", () => {
111+
let tmpDir: string
112+
let mockService: any
113+
114+
beforeEach(async () => {
115+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "checkpoint-test-"))
116+
mockGlobalStorageDir = path.join(tmpDir, "global-storage")
117+
await fs.mkdir(mockGlobalStorageDir, { recursive: true })
118+
119+
// Reset mocks
120+
vitest.clearAllMocks()
121+
122+
// Override the global vscode mock to have a workspace folder
123+
const vscode = await import("vscode")
124+
// @ts-ignore - Mock the workspace.workspaceFolders
125+
vscode.workspace.workspaceFolders = [
126+
{
127+
uri: {
128+
fsPath: "/mock/workspace",
129+
},
130+
},
131+
]
132+
133+
// Mock the checkpoint service
134+
mockService = new EventEmitter()
135+
mockService.baseHash = "mock-base-hash-abc123"
136+
mockService.getCurrentCheckpoint = vitest.fn(() => "mock-current-checkpoint-def456")
137+
mockService.isInitialized = true
138+
mockService.initShadowGit = vitest.fn(() => {
139+
// Simulate the initialize event being emitted after initShadowGit completes
140+
setImmediate(() => {
141+
mockService.emit("initialize")
142+
})
143+
return Promise.resolve()
144+
})
145+
mockService.saveCheckpoint = vitest.fn(() => {
146+
return Promise.resolve({
147+
commit: "mock-checkpoint-hash",
148+
message: "Mock checkpoint",
149+
})
150+
})
151+
152+
// Mock the service creation
153+
;(RepoPerTaskCheckpointService.create as any).mockReturnValue(mockService)
154+
})
155+
156+
afterEach(async () => {
157+
await fs.rm(tmpDir, { recursive: true, force: true })
158+
vitest.restoreAllMocks()
159+
})
160+
161+
describe("Service creation and caching", () => {
162+
it("should create and return a new checkpoint service", async () => {
163+
const task = createMockTask({
164+
taskId: "new-task-123",
165+
hasExistingCheckpoints: false,
166+
})
167+
168+
const service = await getCheckpointService(task)
169+
console.log("Service returned:", service)
170+
expect(service).toBe(mockService)
171+
expect(RepoPerTaskCheckpointService.create).toHaveBeenCalledWith({
172+
taskId: "new-task-123",
173+
shadowDir: mockGlobalStorageDir,
174+
workspaceDir: "/mock/workspace",
175+
log: expect.any(Function),
176+
})
177+
})
178+
179+
it("should return existing service if already initialized", async () => {
180+
const task = createMockTask({
181+
taskId: "existing-service-task",
182+
hasExistingCheckpoints: false,
183+
})
184+
185+
// Set existing checkpoint service
186+
task.checkpointService = mockService
187+
188+
const service = await getCheckpointService(task)
189+
expect(service).toBe(mockService)
190+
191+
// Should not create a new service
192+
expect(RepoPerTaskCheckpointService.create).not.toHaveBeenCalled()
193+
})
194+
195+
it("should return undefined when checkpoints are disabled", async () => {
196+
const task = createMockTask({
197+
taskId: "disabled-task",
198+
hasExistingCheckpoints: false,
199+
enableCheckpoints: false,
200+
})
201+
202+
const service = await getCheckpointService(task)
203+
expect(service).toBeUndefined()
204+
})
205+
})
206+
207+
describe("Service initialization", () => {
208+
it("should call initShadowGit and set up event handlers", async () => {
209+
const task = createMockTask({
210+
taskId: "init-test-task",
211+
hasExistingCheckpoints: false,
212+
})
213+
214+
const service = await getCheckpointService(task)
215+
expect(service).toBe(mockService)
216+
217+
// initShadowGit should be called
218+
expect(mockService.initShadowGit).toHaveBeenCalled()
219+
220+
// Wait for the initialize event to be emitted and the service to be assigned
221+
await new Promise((resolve) => setImmediate(resolve))
222+
223+
// Service should be assigned to task after initialization
224+
expect(task.checkpointService).toBe(mockService)
225+
})
226+
})
227+
})

0 commit comments

Comments
 (0)