mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Await checkpoint saves (except the initial) (#2665)
This commit is contained in:
parent
75a6bc100e
commit
3b19d7a455
19 changed files with 689 additions and 958 deletions
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
1
src/exports/roo-code.d.ts
vendored
1
src/exports/roo-code.d.ts
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
|||
export type { CheckpointServiceOptions } from "./types"
|
||||
|
||||
export { RepoPerTaskCheckpointService } from "./RepoPerTaskCheckpointService"
|
||||
export { RepoPerWorkspaceCheckpointService } from "./RepoPerWorkspaceCheckpointService"
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ export interface WebviewMessage {
|
|||
| "soundVolume"
|
||||
| "diffEnabled"
|
||||
| "enableCheckpoints"
|
||||
| "checkpointStorage"
|
||||
| "browserViewportSize"
|
||||
| "screenshotQuality"
|
||||
| "remoteBrowserHost"
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
import { CheckpointStorage, isCheckpointStorage } from "../schemas"
|
||||
|
||||
export { type CheckpointStorage, isCheckpointStorage }
|
||||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -190,7 +190,6 @@ describe("mergeExtensionState", () => {
|
|||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
enableCheckpoints: true,
|
||||
checkpointStorage: "task",
|
||||
writeDelayMs: 1000,
|
||||
requestDelaySeconds: 5,
|
||||
mode: "default",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue