Skip to content
Closed
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
f379d6b
FCO: resolve conflicts, integrate types + UI, and fix provider.getCur…
hannesrudolph Aug 27, 2025
39cf881
fix: add missing UI translations; id locale consistency; lazy-init VS…
hannesrudolph Aug 27, 2025
896cf9d
fix(windows): use GIT_WORK_TREE for ShadowCheckpointService to avoid …
hannesrudolph Aug 27, 2025
c146ea7
fix(windows): normalize worktree comparison for checkpoints on Window…
hannesrudolph Aug 27, 2025
c39b916
test(windows): relax workspace worktree equality check; log and conti…
hannesrudolph Aug 27, 2025
243a034
fix(settings): move FCO toggle to Experimental, adopt debounced actio…
hannesrudolph Aug 27, 2025
3a62761
test(checkpoints): stabilize diff path on macOS CI by using workspace…
hannesrudolph Aug 27, 2025
06d5674
style(webview): replace remaining inline styles in FilesChangedOvervi…
hannesrudolph Aug 28, 2025
8ac8247
chore(shared): dedupe WebviewMessage.type union; add WebviewMessagePa…
hannesrudolph Aug 28, 2025
b15d6f6
Bug fixes and test cases added
playcations Aug 1, 2025
2f1fc76
Fix type issues in FCOMessageHandler
playcations Aug 31, 2025
879d0d5
Apply remaining FCO edge case fixes from backup branch
playcations Aug 31, 2025
a157ace
Apply missing FCO theming changes from backup branch
playcations Aug 31, 2025
3461836
Chat-only edits are not tracked
playcations Sep 1, 2025
a06779c
Ignore empty tool usage
playcations Sep 1, 2025
91a6587
ix: code quality improvements for FCO feature and address more comments
playcations Sep 1, 2025
901a0e2
Switched to tailwind for FilesChangedOverview
playcations Sep 1, 2025
24d626c
Removed UI from settings, Files Changed now stacked properly under Todo
playcations Sep 1, 2025
94c46f4
possible checkpoint memory leak
playcations Sep 1, 2025
306dd5e
removed initial checkpoint creation
playcations Sep 1, 2025
cb2f668
Files Changed Overview updating after file edits using tool calls.
playcations Sep 2, 2025
29f6e0a
Simplify FileChangeManager architecture with proper accept and rejec…
playcations Sep 3, 2025
7a9c55b
Add type for RooCodeEventName.TaskSpawned (#7465)
mrubens Aug 27, 2025
7e8217b
fix: hide .rooignore'd files from environment details by default (#7369)
roomote[bot] Aug 27, 2025
7bca53c
fix: exclude browser scroll actions from repetition detection (#7471)
roomote[bot] Aug 28, 2025
daa50ed
Fix GPT-5 Responses API issues with condensing and image support (#7067)
daniel-lxs Aug 28, 2025
5150dd9
Bump cloud to 0.25.0 (#7475)
mrubens Aug 28, 2025
ef88be2
feat: add image generation tool with OpenRouter integration (#7474)
daniel-lxs Aug 28, 2025
cf8b15d
Make the default image filename more generic (#7479)
mrubens Aug 28, 2025
761297d
Release v3.26.2 (#7490)
mrubens Aug 28, 2025
76a64cb
Support free imagegen (#7493)
mrubens Aug 28, 2025
0829f05
feat: update OpenRouter API to support input/output modalities and fi…
daniel-lxs Aug 28, 2025
fce4548
Add padding to image model picker (#7494)
mrubens Aug 28, 2025
313a674
fix: prevent dirty state on initial mount in ImageGenerationSettings …
daniel-lxs Aug 28, 2025
cb2efe2
Changeset version bump (#7491)
github-actions[bot] Aug 28, 2025
1e7d33e
Show console logging in vitests when the --no-silent flag is set (#7467)
hassoncs Aug 28, 2025
bdf128a
Move @roo-code/cloud to the Roo-Code repo (#7503)
cte Aug 28, 2025
be4fe29
Refactor the extension bridge (#7515)
cte Aug 29, 2025
1cbb57b
Implement deferred task subscriptions (#7517)
cte Aug 29, 2025
1797b65
feat: add optional input image parameter to image generation tool (#7…
roomote[bot] Aug 29, 2025
666db9a
feat: sync extension bridge settings with cloud (#7535)
roomote[bot] Aug 29, 2025
df556e7
refactor: flatten image generation settings structure (#7536)
daniel-lxs Aug 29, 2025
49d994d
chore: add changeset for v3.26.3 (#7541)
mrubens Aug 29, 2025
b2c2333
Changeset version bump (#7542)
github-actions[bot] Aug 29, 2025
ce12bff
Mode and provider profile selector (#7545)
cte Aug 29, 2025
6655709
Putting the Roo in Roo-leases (#7546)
mrubens Aug 30, 2025
565d2df
Fix evals (#7547)
cte Aug 30, 2025
08d3b6c
fix: special tokens should not break task processing (#7540)
pwilkin Aug 30, 2025
68b68a2
feat: optimize memory usage for image handling in webview (#7556)
daniel-lxs Aug 30, 2025
ec45503
feat: rename Account tab to Cloud tab (#7558)
roomote[bot] Aug 30, 2025
2ba16b7
feat: add Ollama API key support for Turbo mode (#7425)
roomote[bot] Aug 30, 2025
1771dfe
Disconnect extension bridge on logout (#7563)
mrubens Sep 1, 2025
4a128df
Fix claudeCode.notFound translation key (#7571)
chrarnoldus Sep 1, 2025
953da9f
v3.26.4 (#7579)
mrubens Sep 1, 2025
2155473
Update contributors list (#7462)
github-actions[bot] Sep 1, 2025
005ab1b
Changeset version bump (#7580)
github-actions[bot] Sep 1, 2025
84543c5
feat: add configurable embedding batch size for code indexing (#7464)
roomote[bot] Sep 2, 2025
0eb97d0
Shows a pill with the base Roo Code Cloud URL when not pointing to pr…
brunobergher Sep 2, 2025
a0e13fe
Cloud: fix provider syncing (#7603)
jr Sep 2, 2025
8af0d55
fix: add cache reporting support for OpenAI-Native provider (#7602)
hannesrudolph Sep 2, 2025
22e8651
Fix TypeScript compilation errors and FileChangeManager logic after r…
playcations Sep 3, 2025
007b47a
Merge branch 'origin/main' into fco-clean - resolved conflicts
roomote Sep 3, 2025
db6cd50
fix: Add missing types for Files Changed Overview feature
roomote Sep 3, 2025
886e817
fix: Add remaining missing types for Files Changed Overview feature
roomote Sep 3, 2025
eabbe46
fix: Complete type fixes for Files Changed Overview feature
roomote Sep 3, 2025
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
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/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const globalSettingsSchema = z.object({
hasOpenedModeSelector: z.boolean().optional(),
lastModeExportPath: z.string().optional(),
lastModeImportPath: z.string().optional(),
filesChangedEnabled: z.boolean().optional(),
})

export type GlobalSettings = z.infer<typeof globalSettingsSchema>
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export * from "./terminal.js"
export * from "./tool.js"
export * from "./type-fu.js"
export * from "./vscode.js"

export * from "./providers/index.js"
export * from "./file-changes.js"
6 changes: 6 additions & 0 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { Task } from "../task/Task"
import { codebaseSearchTool } from "../tools/codebaseSearchTool"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
import { applyDiffToolLegacy } from "../tools/applyDiffTool"
import { updateFCOAfterEdit } from "../../services/file-changes/updateAfterEdit"

/**
* Processes and presents assistant message content to the user interface.
Expand Down Expand Up @@ -420,6 +421,7 @@ export async function presentAssistantMessage(cline: Task) {
case "write_to_file":
await checkpointSaveAndMark(cline)
await writeToFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
await updateFCOAfterEdit(cline)
break
case "update_todo_list":
await updateTodoListTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
Expand All @@ -440,6 +442,7 @@ export async function presentAssistantMessage(cline: Task) {
if (isMultiFileApplyDiffEnabled) {
await checkpointSaveAndMark(cline)
await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
await updateFCOAfterEdit(cline)
} else {
await checkpointSaveAndMark(cline)
await applyDiffToolLegacy(
Expand All @@ -450,16 +453,19 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult,
removeClosingTag,
)
await updateFCOAfterEdit(cline)
}
break
}
case "insert_content":
await checkpointSaveAndMark(cline)
await insertContentTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
await updateFCOAfterEdit(cline)
break
case "search_and_replace":
await checkpointSaveAndMark(cline)
await searchAndReplaceTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
await updateFCOAfterEdit(cline)
break
case "read_file":
// Check if this model should use the simplified single-file read tool
Expand Down
53 changes: 53 additions & 0 deletions src/core/checkpoints/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { vitest } from "vitest"

export const createMockTask = (options: {
taskId: string
hasExistingCheckpoints?: boolean
enableCheckpoints?: boolean
provider?: any
}) => {
const mockTask = {
taskId: options.taskId,
instanceId: "test-instance",
rootTask: undefined as any,
parentTask: undefined as any,
taskNumber: 1,
workspacePath: "/mock/workspace",
enableCheckpoints: options.enableCheckpoints ?? true,
checkpointService: null as any,
checkpointServiceInitializing: false,
ongoingCheckpointSaves: new Map(),
clineMessages: options.hasExistingCheckpoints
? [{ say: "checkpoint_saved", ts: Date.now(), text: "existing-checkpoint-hash" }]
: [],
providerRef: {
deref: () => options.provider || createMockProvider(),
},
fileContextTracker: {},
todoList: undefined,
}

return mockTask
}

export const createMockProvider = () => ({
getFileChangeManager: vitest.fn(),
ensureFileChangeManager: vitest.fn(),
log: vitest.fn(),
postMessageToWebview: vitest.fn(),
getGlobalState: vitest.fn(),
})

// Mock checkpoint service for testing
export const createMockCheckpointService = () => ({
saveCheckpoint: vitest.fn().mockResolvedValue({
commit: "mock-checkpoint-hash",
message: "Mock checkpoint",
}),
restoreCheckpoint: vitest.fn().mockResolvedValue(true),
getDiff: vitest.fn().mockResolvedValue([]),
getCheckpoints: vitest.fn().mockReturnValue([]),
getCurrentCheckpoint: vitest.fn().mockReturnValue("mock-current-checkpoint"),
initShadowGit: vitest.fn().mockResolvedValue(true),
baseHash: "mock-base-hash",
})
227 changes: 227 additions & 0 deletions src/core/checkpoints/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// Use doMock to apply the mock dynamically
vitest.doMock("../../utils/path", () => ({
getWorkspacePath: vitest.fn(() => {
console.log("getWorkspacePath mock called, returning:", "/mock/workspace")
return "/mock/workspace"
}),
}))

// Mock the RepoPerTaskCheckpointService
vitest.mock("../../../services/checkpoints", () => ({
RepoPerTaskCheckpointService: {
create: vitest.fn(),
},
}))

// Mock the TelemetryService to prevent unhandled rejections
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureCheckpointCreated: vitest.fn(),
captureCheckpointRestored: vitest.fn(),
captureCheckpointDiffed: vitest.fn(),
},
},
}))

import { describe, it, expect, beforeEach, afterEach, vitest } from "vitest"
import * as path from "path"
import * as fs from "fs/promises"
import * as os from "os"
import { EventEmitter } from "events"

// Import these modules after mocks are set up
let getCheckpointService: any
let RepoPerTaskCheckpointService: any

// Set up the imports after mocks
beforeAll(async () => {
const checkpointsModule = await import("../index")
const checkpointServiceModule = await import("../../../services/checkpoints")
getCheckpointService = checkpointsModule.getCheckpointService
RepoPerTaskCheckpointService = checkpointServiceModule.RepoPerTaskCheckpointService
})

// Mock the FileChangeManager to avoid complex dependencies
const mockFileChangeManager = {
_baseline: "HEAD" as string,
getChanges: vitest.fn(),
updateBaseline: vitest.fn(),
setFiles: vitest.fn(),
getLLMOnlyChanges: vitest.fn(),
}

// Create a temporary directory for mock global storage
let mockGlobalStorageDir: string

// Mock the provider
const mockProvider = {
getFileChangeManager: vitest.fn(() => mockFileChangeManager),
log: vitest.fn(),
get context() {
return {
globalStorageUri: {
fsPath: mockGlobalStorageDir,
},
}
},
}

// Mock the Task object with proper typing
const createMockTask = (options: { taskId: string; hasExistingCheckpoints: boolean; enableCheckpoints?: boolean }) => {
const mockTask = {
taskId: options.taskId,
instanceId: "test-instance",
rootTask: undefined as any,
parentTask: undefined as any,
taskNumber: 1,
workspacePath: "/mock/workspace",
enableCheckpoints: options.enableCheckpoints ?? true,
checkpointService: null as any,
checkpointServiceInitializing: false,
ongoingCheckpointSaves: new Map(),
clineMessages: options.hasExistingCheckpoints
? [{ say: "checkpoint_saved", ts: Date.now(), text: "existing-checkpoint-hash" }]
: [],
providerRef: {
deref: () => mockProvider,
},
fileContextTracker: {},
// Add minimal required properties to satisfy Task interface
todoList: undefined,
userMessageContent: "",
apiConversationHistory: [],
customInstructions: "",
alwaysAllowReadOnly: false,
alwaysAllowWrite: false,
alwaysAllowExecute: false,
alwaysAllowBrowser: false,
alwaysAllowMcp: false,
createdAt: Date.now(),
historyErrors: [],
askResponse: undefined,
askResponseText: "",
abort: vitest.fn(),
isAborting: false,
} as any // Cast to any to avoid needing to implement all Task methods
return mockTask
}

describe("getCheckpointService orchestration", () => {
let tmpDir: string
let mockService: any

beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "checkpoint-test-"))
mockGlobalStorageDir = path.join(tmpDir, "global-storage")
await fs.mkdir(mockGlobalStorageDir, { recursive: true })

// Reset mocks
vitest.clearAllMocks()

// Override the global vscode mock to have a workspace folder
const vscode = await import("vscode")
// @ts-ignore - Mock the workspace.workspaceFolders
vscode.workspace.workspaceFolders = [
{
uri: {
fsPath: "/mock/workspace",
},
},
]

// Mock the checkpoint service
mockService = new EventEmitter()
mockService.baseHash = "mock-base-hash-abc123"
mockService.getCurrentCheckpoint = vitest.fn(() => "mock-current-checkpoint-def456")
mockService.isInitialized = true
mockService.initShadowGit = vitest.fn(() => {
// Simulate the initialize event being emitted after initShadowGit completes
setImmediate(() => {
mockService.emit("initialize")
})
return Promise.resolve()
})
mockService.saveCheckpoint = vitest.fn(() => {
return Promise.resolve({
commit: "mock-checkpoint-hash",
message: "Mock checkpoint",
})
})

// Mock the service creation
;(RepoPerTaskCheckpointService.create as any).mockReturnValue(mockService)
})

afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true })
vitest.restoreAllMocks()
})

describe("Service creation and caching", () => {
it("should create and return a new checkpoint service", async () => {
const task = createMockTask({
taskId: "new-task-123",
hasExistingCheckpoints: false,
})

const service = await getCheckpointService(task)
console.log("Service returned:", service)
expect(service).toBe(mockService)
expect(RepoPerTaskCheckpointService.create).toHaveBeenCalledWith({
taskId: "new-task-123",
shadowDir: mockGlobalStorageDir,
workspaceDir: "/mock/workspace",
log: expect.any(Function),
})
})

it("should return existing service if already initialized", async () => {
const task = createMockTask({
taskId: "existing-service-task",
hasExistingCheckpoints: false,
})

// Set existing checkpoint service
task.checkpointService = mockService

const service = await getCheckpointService(task)
expect(service).toBe(mockService)

// Should not create a new service
expect(RepoPerTaskCheckpointService.create).not.toHaveBeenCalled()
})

it("should return undefined when checkpoints are disabled", async () => {
const task = createMockTask({
taskId: "disabled-task",
hasExistingCheckpoints: false,
enableCheckpoints: false,
})

const service = await getCheckpointService(task)
expect(service).toBeUndefined()
})
})

describe("Service initialization", () => {
it("should call initShadowGit and set up event handlers", async () => {
const task = createMockTask({
taskId: "init-test-task",
hasExistingCheckpoints: false,
})

const service = await getCheckpointService(task)
expect(service).toBe(mockService)

// initShadowGit should be called
expect(mockService.initShadowGit).toHaveBeenCalled()

// Wait for the initialize event to be emitted and the service to be assigned
await new Promise((resolve) => setImmediate(resolve))

// Service should be assigned to task after initialization
expect(task.checkpointService).toBe(mockService)
})
})
})
Loading