Await checkpoint saves (except the initial) (#2665)

This commit is contained in:
Chris Estreich 2025-04-15 16:57:12 -07:00 committed by GitHub
parent 75a6bc100e
commit 3b19d7a455
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 689 additions and 958 deletions

View file

@ -16,11 +16,7 @@ import { TokenUsage } from "../schemas"
import { ApiHandler, buildApiHandler } from "../api"
import { ApiStream } from "../api/transform/stream"
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
import {
CheckpointServiceOptions,
RepoPerTaskCheckpointService,
RepoPerWorkspaceCheckpointService,
} from "../services/checkpoints"
import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../services/checkpoints"
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
import { fetchInstructionsTool } from "./tools/fetchInstructionsTool"
import { listFilesTool } from "./tools/listFilesTool"
@ -30,7 +26,6 @@ import { Terminal } from "../integrations/terminal/Terminal"
import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
import { listFiles } from "../services/glob/list-files"
import { CheckpointStorage } from "../shared/checkpoints"
import { ApiConfiguration } from "../shared/api"
import { findLastIndex } from "../shared/array"
import { combineApiRequests } from "../shared/combineApiRequests"
@ -104,7 +99,6 @@ export type ClineOptions = {
customInstructions?: string
enableDiff?: boolean
enableCheckpoints?: boolean
checkpointStorage?: CheckpointStorage
fuzzyMatchThreshold?: number
consecutiveMistakeLimit?: number
task?: string
@ -162,8 +156,8 @@ export class Cline extends EventEmitter<ClineEvents> {
// checkpoints
private enableCheckpoints: boolean
private checkpointStorage: CheckpointStorage
private checkpointService?: RepoPerTaskCheckpointService | RepoPerWorkspaceCheckpointService
private checkpointService?: RepoPerTaskCheckpointService
private checkpointServiceInitializing = false
// streaming
isWaitingForFirstChunk = false
@ -184,7 +178,6 @@ export class Cline extends EventEmitter<ClineEvents> {
customInstructions,
enableDiff = false,
enableCheckpoints = true,
checkpointStorage = "task",
fuzzyMatchThreshold = 1.0,
consecutiveMistakeLimit = 3,
task,
@ -223,7 +216,6 @@ export class Cline extends EventEmitter<ClineEvents> {
this.providerRef = new WeakRef(provider)
this.diffViewProvider = new DiffViewProvider(this.cwd)
this.enableCheckpoints = enableCheckpoints
this.checkpointStorage = checkpointStorage
this.rootTask = rootTask
this.parentTask = parentTask
@ -1680,9 +1672,11 @@ export class Cline extends EventEmitter<ClineEvents> {
}
const recentlyModifiedFiles = this.fileContextTracker.getAndClearCheckpointPossibleFile()
if (recentlyModifiedFiles.length > 0) {
// TODO: we can track what file changes were made and only checkpoint those files, this will be save storage
this.checkpointSave()
// TODO: We can track what file changes were made and only
// checkpoint those files, this will be save storage.
await this.checkpointSave()
}
/*
@ -2397,6 +2391,11 @@ export class Cline extends EventEmitter<ClineEvents> {
return this.checkpointService
}
if (this.checkpointServiceInitializing) {
console.log("[Cline#getCheckpointService] checkpoint service is still initializing")
return undefined
}
const log = (message: string) => {
console.log(message)
@ -2407,11 +2406,13 @@ export class Cline extends EventEmitter<ClineEvents> {
}
}
console.log("[Cline#getCheckpointService] initializing checkpoints service")
try {
const workspaceDir = getWorkspacePath()
if (!workspaceDir) {
log("[Cline#initializeCheckpoints] workspace folder not found, disabling checkpoints")
log("[Cline#getCheckpointService] workspace folder not found, disabling checkpoints")
this.enableCheckpoints = false
return undefined
}
@ -2419,7 +2420,7 @@ export class Cline extends EventEmitter<ClineEvents> {
const globalStorageDir = this.providerRef.deref()?.context.globalStorageUri.fsPath
if (!globalStorageDir) {
log("[Cline#initializeCheckpoints] globalStorageDir not found, disabling checkpoints")
log("[Cline#getCheckpointService] globalStorageDir not found, disabling checkpoints")
this.enableCheckpoints = false
return undefined
}
@ -2431,28 +2432,26 @@ export class Cline extends EventEmitter<ClineEvents> {
log,
}
// Only `task` is supported at the moment until we figure out how
// to fully isolate the `workspace` variant.
// const service =
// this.checkpointStorage === "task"
// ? RepoPerTaskCheckpointService.create(options)
// : RepoPerWorkspaceCheckpointService.create(options)
const service = RepoPerTaskCheckpointService.create(options)
this.checkpointServiceInitializing = true
service.on("initialize", () => {
log("[Cline#getCheckpointService] service initialized")
try {
const isCheckpointNeeded =
typeof this.clineMessages.find(({ say }) => say === "checkpoint_saved") === "undefined"
this.checkpointService = service
this.checkpointServiceInitializing = false
if (isCheckpointNeeded) {
log("[Cline#initializeCheckpoints] no checkpoints found, saving initial checkpoint")
log("[Cline#getCheckpointService] no checkpoints found, saving initial checkpoint")
this.checkpointSave()
}
} catch (err) {
log("[Cline#initializeCheckpoints] caught error in on('initialize'), disabling checkpoints")
log("[Cline#getCheckpointService] caught error in on('initialize'), disabling checkpoints")
this.enableCheckpoints = false
}
})
@ -2462,21 +2461,23 @@ export class Cline extends EventEmitter<ClineEvents> {
this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: to })
this.say("checkpoint_saved", to, undefined, undefined, { isFirst, from, to }).catch((err) => {
log("[Cline#initializeCheckpoints] caught unexpected error in say('checkpoint_saved')")
log("[Cline#getCheckpointService] caught unexpected error in say('checkpoint_saved')")
console.error(err)
})
} catch (err) {
log(
"[Cline#initializeCheckpoints] caught unexpected error in on('checkpoint'), disabling checkpoints",
"[Cline#getCheckpointService] caught unexpected error in on('checkpoint'), disabling checkpoints",
)
console.error(err)
this.enableCheckpoints = false
}
})
log("[Cline#getCheckpointService] initializing shadow git")
service.initShadowGit().catch((err) => {
log(
`[Cline#initializeCheckpoints] caught unexpected error in initShadowGit, disabling checkpoints (${err.message})`,
`[Cline#getCheckpointService] caught unexpected error in initShadowGit, disabling checkpoints (${err.message})`,
)
console.error(err)
this.enableCheckpoints = false
@ -2484,7 +2485,7 @@ export class Cline extends EventEmitter<ClineEvents> {
return service
} catch (err) {
log("[Cline#initializeCheckpoints] caught unexpected error, disabling checkpoints")
log("[Cline#getCheckpointService] caught unexpected error, disabling checkpoints")
this.enableCheckpoints = false
return undefined
}
@ -2508,6 +2509,7 @@ export class Cline extends EventEmitter<ClineEvents> {
},
{ interval, timeout },
)
return service
} catch (err) {
return undefined
@ -2569,7 +2571,7 @@ export class Cline extends EventEmitter<ClineEvents> {
}
}
public checkpointSave() {
public async checkpointSave() {
const service = this.getCheckpointService()
if (!service) {
@ -2580,6 +2582,7 @@ export class Cline extends EventEmitter<ClineEvents> {
this.providerRef
.deref()
?.log("[checkpointSave] checkpoints didn't initialize in time, disabling checkpoints for this task")
this.enableCheckpoints = false
return
}
@ -2587,7 +2590,7 @@ export class Cline extends EventEmitter<ClineEvents> {
telemetryService.captureCheckpointCreated(this.taskId)
// Start the checkpoint process in the background.
service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`).catch((err) => {
return service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`).catch((err) => {
console.error("[Cline#checkpointSave] caught unexpected error, disabling checkpoints", err)
this.enableCheckpoints = false
})

View file

@ -483,7 +483,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
| "customInstructions"
| "enableDiff"
| "enableCheckpoints"
| "checkpointStorage"
| "fuzzyMatchThreshold"
| "consecutiveMistakeLimit"
| "experiments"
@ -495,7 +494,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
customModePrompts,
diffEnabled: enableDiff,
enableCheckpoints,
checkpointStorage,
fuzzyMatchThreshold,
mode,
customInstructions: globalInstructions,
@ -511,7 +509,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
customInstructions: effectiveInstructions,
enableDiff,
enableCheckpoints,
checkpointStorage,
fuzzyMatchThreshold,
task,
images,
@ -540,7 +537,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
customModePrompts,
diffEnabled: enableDiff,
enableCheckpoints,
checkpointStorage,
fuzzyMatchThreshold,
mode,
customInstructions: globalInstructions,
@ -550,38 +546,12 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
const modePrompt = customModePrompts?.[mode] as PromptComponent
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
const taskId = historyItem.id
const globalStorageDir = this.contextProxy.globalStorageUri.fsPath
const workspaceDir = this.cwd
const checkpoints: Pick<ClineOptions, "enableCheckpoints" | "checkpointStorage"> = {
enableCheckpoints,
checkpointStorage,
}
if (enableCheckpoints) {
try {
checkpoints.checkpointStorage = await ShadowCheckpointService.getTaskStorage({
taskId,
globalStorageDir,
workspaceDir,
})
this.log(
`[ClineProvider#initClineWithHistoryItem] Using ${checkpoints.checkpointStorage} storage for ${taskId}`,
)
} catch (error) {
checkpoints.enableCheckpoints = false
this.log(`[ClineProvider#initClineWithHistoryItem] Error getting task storage: ${error.message}`)
}
}
const cline = new Cline({
provider: this,
apiConfiguration,
customInstructions: effectiveInstructions,
enableDiff,
...checkpoints,
enableCheckpoints,
fuzzyMatchThreshold,
historyItem,
experiments,
@ -1210,7 +1180,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
ttsSpeed,
diffEnabled,
enableCheckpoints,
checkpointStorage,
taskHistory,
soundVolume,
browserViewportSize,
@ -1282,7 +1251,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
ttsSpeed: ttsSpeed ?? 1.0,
diffEnabled: diffEnabled ?? true,
enableCheckpoints: enableCheckpoints ?? true,
checkpointStorage: checkpointStorage ?? "task",
shouldShowAnnouncement:
telemetrySetting !== "unset" && lastShownAnnouncementId !== this.latestAnnouncementId,
allowedCommands,
@ -1377,7 +1345,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
ttsSpeed: stateValues.ttsSpeed ?? 1.0,
diffEnabled: stateValues.diffEnabled ?? true,
enableCheckpoints: stateValues.enableCheckpoints ?? true,
checkpointStorage: stateValues.checkpointStorage ?? "task",
soundVolume: stateValues.soundVolume,
browserViewportSize: stateValues.browserViewportSize ?? "900x600",
screenshotQuality: stateValues.screenshotQuality ?? 75,

View file

@ -407,7 +407,6 @@ describe("ClineProvider", () => {
ttsEnabled: false,
diffEnabled: false,
enableCheckpoints: false,
checkpointStorage: "task",
writeDelayMs: 1000,
browserViewportSize: "900x600",
fuzzyMatchThreshold: 1.0,
@ -829,7 +828,6 @@ describe("ClineProvider", () => {
mode: "code",
diffEnabled: true,
enableCheckpoints: false,
checkpointStorage: "task",
fuzzyMatchThreshold: 1.0,
experiments: experimentDefault,
} as any)
@ -848,7 +846,6 @@ describe("ClineProvider", () => {
customInstructions: modeCustomInstructions,
enableDiff: true,
enableCheckpoints: false,
checkpointStorage: "task",
fuzzyMatchThreshold: 1.0,
task: "Test task",
experiments: experimentDefault,

View file

@ -4,7 +4,7 @@ import pWaitFor from "p-wait-for"
import * as vscode from "vscode"
import { ClineProvider } from "./ClineProvider"
import { CheckpointStorage, Language, ApiConfigMeta } from "../../schemas"
import { Language, ApiConfigMeta } from "../../schemas"
import { changeLanguage, t } from "../../i18n"
import { ApiConfiguration } from "../../shared/api"
import { supportPrompt } from "../../shared/support-prompt"
@ -655,12 +655,6 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
await updateGlobalState("enableCheckpoints", enableCheckpoints)
await provider.postStateToWebview()
break
case "checkpointStorage":
console.log(`[ClineProvider] checkpointStorage: ${message.text}`)
const checkpointStorage = message.text ?? "task"
await updateGlobalState("checkpointStorage", checkpointStorage as CheckpointStorage)
await provider.postStateToWebview()
break
case "browserViewportSize":
const browserViewportSize = message.text ?? "900x600"
await updateGlobalState("browserViewportSize", browserViewportSize)

View file

@ -259,7 +259,6 @@ type GlobalSettings = {
remoteBrowserHost?: string | undefined
cachedChromeHostUrl?: string | undefined
enableCheckpoints?: boolean | undefined
checkpointStorage?: ("task" | "workspace") | undefined
showGreeting?: boolean | undefined
ttsEnabled?: boolean | undefined
ttsSpeed?: number | undefined

View file

@ -262,7 +262,6 @@ type GlobalSettings = {
remoteBrowserHost?: string | undefined
cachedChromeHostUrl?: string | undefined
enableCheckpoints?: boolean | undefined
checkpointStorage?: ("task" | "workspace") | undefined
showGreeting?: boolean | undefined
ttsEnabled?: boolean | undefined
ttsSpeed?: number | undefined

View file

@ -44,19 +44,6 @@ export const toolGroupsSchema = z.enum(toolGroups)
export type ToolGroup = z.infer<typeof toolGroupsSchema>
/**
* CheckpointStorage
*/
export const checkpointStorages = ["task", "workspace"] as const
export const checkpointStoragesSchema = z.enum(checkpointStorages)
export type CheckpointStorage = z.infer<typeof checkpointStoragesSchema>
export const isCheckpointStorage = (value: string): value is CheckpointStorage =>
checkpointStorages.includes(value as CheckpointStorage)
/**
* Language
*/
@ -536,7 +523,6 @@ export const globalSettingsSchema = z.object({
cachedChromeHostUrl: z.string().optional(),
enableCheckpoints: z.boolean().optional(),
checkpointStorage: checkpointStoragesSchema.optional(),
showGreeting: z.boolean().optional(),
@ -614,7 +600,6 @@ const globalSettingsRecord: GlobalSettingsRecord = {
remoteBrowserHost: undefined,
enableCheckpoints: undefined,
checkpointStorage: undefined,
showGreeting: undefined,

View file

@ -1,75 +0,0 @@
import * as path from "path"
import { CheckpointServiceOptions } from "./types"
import { ShadowCheckpointService } from "./ShadowCheckpointService"
export class RepoPerWorkspaceCheckpointService extends ShadowCheckpointService {
private async checkoutTaskBranch(source: string) {
if (!this.git) {
throw new Error("Shadow git repo not initialized")
}
const startTime = Date.now()
const branch = `roo-${this.taskId}`
const currentBranch = await this.git.revparse(["--abbrev-ref", "HEAD"])
if (currentBranch === branch) {
return
}
this.log(`[${this.constructor.name}#checkoutTaskBranch{${source}}] checking out ${branch}`)
const branches = await this.git.branchLocal()
let exists = branches.all.includes(branch)
if (!exists) {
await this.git.checkoutLocalBranch(branch)
} else {
await this.git.checkout(branch)
}
const duration = Date.now() - startTime
this.log(
`[${this.constructor.name}#checkoutTaskBranch{${source}}] ${exists ? "checked out" : "created"} branch "${branch}" in ${duration}ms`,
)
}
override async initShadowGit() {
return await super.initShadowGit(() => this.checkoutTaskBranch("initShadowGit"))
}
override async saveCheckpoint(message: string) {
await this.checkoutTaskBranch("saveCheckpoint")
return super.saveCheckpoint(message)
}
override async restoreCheckpoint(commitHash: string) {
await this.checkoutTaskBranch("restoreCheckpoint")
await super.restoreCheckpoint(commitHash)
}
override async getDiff({ from, to }: { from?: string; to?: string }) {
if (!this.git) {
throw new Error("Shadow git repo not initialized")
}
await this.checkoutTaskBranch("getDiff")
if (!from && to) {
from = `${to}~`
}
return super.getDiff({ from, to })
}
public static create({ taskId, workspaceDir, shadowDir, log = console.log }: CheckpointServiceOptions) {
const workspaceHash = this.hashWorkspaceDir(workspaceDir)
return new RepoPerWorkspaceCheckpointService(
taskId,
path.join(shadowDir, "checkpoints", workspaceHash),
workspaceDir,
log,
)
}
}

View file

@ -5,11 +5,10 @@ import crypto from "crypto"
import EventEmitter from "events"
import simpleGit, { SimpleGit } from "simple-git"
import { globby } from "globby"
import pWaitFor from "p-wait-for"
import { fileExistsAtPath } from "../../utils/fs"
import { CheckpointStorage } from "../../shared/checkpoints"
import { executeRipgrep } from "../../services/search/file-search"
import { GIT_DISABLED_SUFFIX } from "./constants"
import { CheckpointDiff, CheckpointResult, CheckpointEventMap } from "./types"
@ -150,39 +149,54 @@ export abstract class ShadowCheckpointService extends EventEmitter {
// nested git repos to work around git's requirement of using submodules for
// nested repos.
private async renameNestedGitRepos(disable: boolean) {
// Find all .git directories that are not at the root level.
const gitPaths = await globby("**/.git" + (disable ? "" : GIT_DISABLED_SUFFIX), {
cwd: this.workspaceDir,
onlyDirectories: true,
ignore: [".git"], // Ignore root level .git.
dot: true,
markDirectories: false,
})
try {
// Find all .git directories that are not at the root level.
const gitDir = ".git" + (disable ? "" : GIT_DISABLED_SUFFIX)
const args = ["--files", "--hidden", "--follow", "-g", `**/${gitDir}/HEAD`, this.workspaceDir]
// For each nested .git directory, rename it based on operation.
for (const gitPath of gitPaths) {
const fullPath = path.join(this.workspaceDir, gitPath)
let newPath: string
const gitPaths = await (
await executeRipgrep({ args, workspacePath: this.workspaceDir })
).filter(({ type, path }) => type === "folder" && path.includes(".git") && !path.startsWith(".git"))
if (disable) {
newPath = fullPath + GIT_DISABLED_SUFFIX
} else {
newPath = fullPath.endsWith(GIT_DISABLED_SUFFIX)
? fullPath.slice(0, -GIT_DISABLED_SUFFIX.length)
: fullPath
}
try {
await fs.rename(fullPath, newPath)
this.log(
`[${this.constructor.name}#renameNestedGitRepos] ${disable ? "disabled" : "enabled"} nested git repo ${gitPath}`,
)
} catch (error) {
this.log(
`[${this.constructor.name}#renameNestedGitRepos] failed to ${disable ? "disable" : "enable"} nested git repo ${gitPath}: ${error instanceof Error ? error.message : String(error)}`,
)
// For each nested .git directory, rename it based on operation.
for (const gitPath of gitPaths) {
if (gitPath.path.startsWith(".git")) {
continue
}
const currentPath = path.join(this.workspaceDir, gitPath.path)
let newPath: string
if (disable) {
newPath = !currentPath.endsWith(GIT_DISABLED_SUFFIX)
? currentPath + GIT_DISABLED_SUFFIX
: currentPath
} else {
newPath = currentPath.endsWith(GIT_DISABLED_SUFFIX)
? currentPath.slice(0, -GIT_DISABLED_SUFFIX.length)
: currentPath
}
if (currentPath === newPath) {
continue
}
try {
await fs.rename(currentPath, newPath)
this.log(
`[${this.constructor.name}#renameNestedGitRepos] ${disable ? "disabled" : "enabled"} nested git repo ${currentPath}`,
)
} catch (error) {
this.log(
`[${this.constructor.name}#renameNestedGitRepos] failed to ${disable ? "disable" : "enable"} nested git repo ${currentPath}: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
} catch (error) {
this.log(
`[${this.constructor.name}#renameNestedGitRepos] failed to ${disable ? "disable" : "enable"} nested git repos: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
@ -344,39 +358,6 @@ export abstract class ShadowCheckpointService extends EventEmitter {
return path.join(globalStorageDir, "checkpoints", this.hashWorkspaceDir(workspaceDir))
}
public static async getTaskStorage({
taskId,
globalStorageDir,
workspaceDir,
}: {
taskId: string
globalStorageDir: string
workspaceDir: string
}): Promise<CheckpointStorage | undefined> {
// Is there a checkpoints repo in the task directory?
const taskRepoDir = this.taskRepoDir({ taskId, globalStorageDir })
if (await fileExistsAtPath(taskRepoDir)) {
return "task"
}
// Does the workspace checkpoints repo have a branch for this task?
const workspaceRepoDir = this.workspaceRepoDir({ globalStorageDir, workspaceDir })
if (!(await fileExistsAtPath(workspaceRepoDir))) {
return undefined
}
const git = simpleGit(workspaceRepoDir)
const branches = await git.branchLocal()
if (branches.all.includes(`roo-${taskId}`)) {
return "workspace"
}
return undefined
}
public static async deleteTask({
taskId,
globalStorageDir,
@ -386,23 +367,15 @@ export abstract class ShadowCheckpointService extends EventEmitter {
globalStorageDir: string
workspaceDir: string
}) {
const storage = await this.getTaskStorage({ taskId, globalStorageDir, workspaceDir })
const workspaceRepoDir = this.workspaceRepoDir({ globalStorageDir, workspaceDir })
const branchName = `roo-${taskId}`
const git = simpleGit(workspaceRepoDir)
const success = await this.deleteBranch(git, branchName)
if (storage === "task") {
const taskRepoDir = this.taskRepoDir({ taskId, globalStorageDir })
await fs.rm(taskRepoDir, { recursive: true, force: true })
console.log(`[${this.name}#deleteTask.${taskId}] removed ${taskRepoDir}`)
} else if (storage === "workspace") {
const workspaceRepoDir = this.workspaceRepoDir({ globalStorageDir, workspaceDir })
const branchName = `roo-${taskId}`
const git = simpleGit(workspaceRepoDir)
const success = await this.deleteBranch(git, branchName)
if (success) {
console.log(`[${this.name}#deleteTask.${taskId}] deleted branch ${branchName}`)
} else {
console.error(`[${this.name}#deleteTask.${taskId}] failed to delete branch ${branchName}`)
}
if (success) {
console.log(`[${this.name}#deleteTask.${taskId}] deleted branch ${branchName}`)
} else {
console.error(`[${this.name}#deleteTask.${taskId}] failed to delete branch ${branchName}`)
}
}

View file

@ -1,4 +1,3 @@
export type { CheckpointServiceOptions } from "./types"
export { RepoPerTaskCheckpointService } from "./RepoPerTaskCheckpointService"
export { RepoPerWorkspaceCheckpointService } from "./RepoPerWorkspaceCheckpointService"

View file

@ -6,35 +6,29 @@ import * as readline from "readline"
import { byLengthAsc, Fzf } from "fzf"
import { getBinPath } from "../ripgrep"
async function executeRipgrepForFiles(
rgPath: string,
workspacePath: string,
limit: number = 5000,
): Promise<{ path: string; type: "file" | "folder"; label?: string }[]> {
export type FileResult = { path: string; type: "file" | "folder"; label?: string }
export async function executeRipgrep({
args,
workspacePath,
limit = 500,
}: {
args: string[]
workspacePath: string
limit?: number
}): Promise<FileResult[]> {
const rgPath = await getBinPath(vscode.env.appRoot)
if (!rgPath) {
throw new Error(`ripgrep not found: ${rgPath}`)
}
return new Promise((resolve, reject) => {
const args = [
"--files",
"--follow",
"--hidden",
"-g",
"!**/node_modules/**",
"-g",
"!**/.git/**",
"-g",
"!**/out/**",
"-g",
"!**/dist/**",
workspacePath,
]
const rgProcess = childProcess.spawn(rgPath, args)
const rl = readline.createInterface({
input: rgProcess.stdout,
crlfDelay: Infinity,
})
const rl = readline.createInterface({ input: rgProcess.stdout, crlfDelay: Infinity })
const fileResults: FileResult[] = []
const dirSet = new Set<string>() // Track unique directory paths.
const fileResults: { path: string; type: "file" | "folder"; label?: string }[] = []
const dirSet = new Set<string>() // Track unique directory paths
let count = 0
rl.on("line", (line) => {
@ -42,15 +36,12 @@ async function executeRipgrepForFiles(
try {
const relativePath = path.relative(workspacePath, line)
// Add the file itself
fileResults.push({
path: relativePath,
type: "file",
label: path.basename(relativePath),
})
// Add the file itself.
fileResults.push({ path: relativePath, type: "file", label: path.basename(relativePath) })
// Extract and store all parent directory paths
// Extract and store all parent directory paths.
let dirPath = path.dirname(relativePath)
while (dirPath && dirPath !== "." && dirPath !== "/") {
dirSet.add(dirPath)
dirPath = path.dirname(dirPath)
@ -58,7 +49,7 @@ async function executeRipgrepForFiles(
count++
} catch (error) {
// Silently ignore errors processing individual paths
// Silently ignore errors processing individual paths.
}
} else {
rl.close()
@ -67,6 +58,7 @@ async function executeRipgrepForFiles(
})
let errorOutput = ""
rgProcess.stderr.on("data", (data) => {
errorOutput += data.toString()
})
@ -75,14 +67,14 @@ async function executeRipgrepForFiles(
if (errorOutput && fileResults.length === 0) {
reject(new Error(`ripgrep process error: ${errorOutput}`))
} else {
// Convert directory set to array of directory objects
// Convert directory set to array of directory objects.
const dirResults = Array.from(dirSet).map((dirPath) => ({
path: dirPath,
type: "folder" as const,
label: path.basename(dirPath),
}))
// Combine files and directories and resolve
// Combine files and directories and resolve.
resolve([...fileResults, ...dirResults])
}
})
@ -93,21 +85,36 @@ async function executeRipgrepForFiles(
})
}
export async function executeRipgrepForFiles(
workspacePath: string,
limit: number = 5000,
): Promise<{ path: string; type: "file" | "folder"; label?: string }[]> {
const args = [
"--files",
"--follow",
"--hidden",
"-g",
"!**/node_modules/**",
"-g",
"!**/.git/**",
"-g",
"!**/out/**",
"-g",
"!**/dist/**",
workspacePath,
]
return executeRipgrep({ args, workspacePath, limit })
}
export async function searchWorkspaceFiles(
query: string,
workspacePath: string,
limit: number = 20,
): Promise<{ path: string; type: "file" | "folder"; label?: string }[]> {
try {
const vscodeAppRoot = vscode.env.appRoot
const rgPath = await getBinPath(vscodeAppRoot)
if (!rgPath) {
throw new Error("Could not find ripgrep binary")
}
// Get all files and directories (from our modified function)
const allItems = await executeRipgrepForFiles(rgPath, workspacePath, 5000)
const allItems = await executeRipgrepForFiles(workspacePath, 5000)
// If no query, just return the top items
if (!query.trim()) {

View file

@ -5,7 +5,6 @@ import {
ProviderSettings as ApiConfiguration,
HistoryItem,
ModeConfig,
CheckpointStorage,
TelemetrySetting,
ExperimentId,
ClineAsk,
@ -142,7 +141,6 @@ export type ExtensionState = Pick<
| "remoteBrowserEnabled"
| "remoteBrowserHost"
// | "enableCheckpoints" // Optional in GlobalSettings, required here.
// | "checkpointStorage" // Optional in GlobalSettings, required here.
| "showGreeting"
| "ttsEnabled"
| "ttsSpeed"
@ -187,7 +185,6 @@ export type ExtensionState = Pick<
requestDelaySeconds: number
enableCheckpoints: boolean
checkpointStorage: CheckpointStorage
maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500)
maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500)
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings

View file

@ -64,7 +64,6 @@ export interface WebviewMessage {
| "soundVolume"
| "diffEnabled"
| "enableCheckpoints"
| "checkpointStorage"
| "browserViewportSize"
| "screenshotQuality"
| "remoteBrowserHost"

View file

@ -1,3 +0,0 @@
import { CheckpointStorage, isCheckpointStorage } from "../schemas"
export { type CheckpointStorage, isCheckpointStorage }

View file

@ -3,24 +3,16 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { GitBranch } from "lucide-react"
import { CheckpointStorage } from "../../../../src/shared/checkpoints"
import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
type CheckpointSettingsProps = HTMLAttributes<HTMLDivElement> & {
enableCheckpoints?: boolean
checkpointStorage?: CheckpointStorage
setCachedStateField: SetCachedStateField<"enableCheckpoints" | "checkpointStorage">
setCachedStateField: SetCachedStateField<"enableCheckpoints">
}
export const CheckpointSettings = ({
enableCheckpoints,
checkpointStorage = "task",
setCachedStateField,
...props
}: CheckpointSettingsProps) => {
export const CheckpointSettings = ({ enableCheckpoints, setCachedStateField, ...props }: CheckpointSettingsProps) => {
const { t } = useAppTranslation()
return (
<div {...props}>

View file

@ -112,7 +112,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
browserToolEnabled,
browserViewportSize,
enableCheckpoints,
checkpointStorage,
diffEnabled,
experiments,
fuzzyMatchThreshold,
@ -235,7 +234,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
vscode.postMessage({ type: "soundVolume", value: soundVolume })
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
vscode.postMessage({ type: "checkpointStorage", text: checkpointStorage })
vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
vscode.postMessage({ type: "remoteBrowserHost", text: remoteBrowserHost })
vscode.postMessage({ type: "remoteBrowserEnabled", bool: remoteBrowserEnabled })
@ -466,7 +464,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
<div ref={checkpointsRef}>
<CheckpointSettings
enableCheckpoints={enableCheckpoints}
checkpointStorage={checkpointStorage}
setCachedStateField={setCachedStateField}
/>
</div>

View file

@ -132,7 +132,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
ttsSpeed: 1.0,
diffEnabled: false,
enableCheckpoints: true,
checkpointStorage: "task",
fuzzyMatchThreshold: 1.0,
language: "en", // Default language code
writeDelayMs: 1000,

View file

@ -190,7 +190,6 @@ describe("mergeExtensionState", () => {
taskHistory: [],
shouldShowAnnouncement: false,
enableCheckpoints: true,
checkpointStorage: "task",
writeDelayMs: 1000,
requestDelaySeconds: 5,
mode: "default",