mirror of
https://github.com/cline/cline.git
synced 2026-09-12 00:50:27 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3661d9b9a | ||
|
|
3b5556ae6c |
@@ -0,0 +1,18 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
## Checkpoints 2.0: **User-Configurable Checkpoints** Settings
|
||||
|
||||
### New Features
|
||||
- **Global File Exclusions List:** Comprehensive list of files to exclude (build outputs, media files, logs, etc.)
|
||||
- **Checkpoint Cleanup:** New command to delete all checkpoints and reclaim storage space
|
||||
|
||||
### Quality of Life
|
||||
- **Settings Panel:** Manage all checkpoint settings in one convenient location, providing space for easily adding future checkpoint settings
|
||||
- **Space Management:** More control over what gets saved in your checkpoints
|
||||
- **Smart Defaults:** Out-of-the-box checkpoint settings that work for most projects
|
||||
|
||||
Settings are preserved when updating, your existing configuration will work as expected.
|
||||
|
||||
---
|
||||
@@ -157,11 +157,6 @@
|
||||
"default": "full",
|
||||
"description": "Controls MCP inclusion in prompts, reduces token usage if you only need access to certain functionality."
|
||||
},
|
||||
"cline.enableCheckpoints": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enables extension to save checkpoints of workspace throughout the task."
|
||||
},
|
||||
"cline.disableBrowserTool": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
|
||||
@@ -9,6 +9,8 @@ import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { buildApiHandler } from "../../api"
|
||||
import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
|
||||
import { CheckpointSettingsManager } from "../../integrations/checkpoints/CheckpointSettings"
|
||||
import { deleteAllCheckpoints } from "../../integrations/checkpoints/CheckpointUtils"
|
||||
import { downloadTask } from "../../integrations/misc/export-markdown"
|
||||
import { openFile, openImage } from "../../integrations/misc/open-file"
|
||||
import { fetchOpenGraphData, isImageUrl } from "../../integrations/misc/link-preview"
|
||||
@@ -794,6 +796,48 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "getCheckpointSettings": {
|
||||
const settings = await CheckpointSettingsManager.getInstance().getSettings()
|
||||
await this.postMessageToWebview({
|
||||
type: "setCheckpointSettings",
|
||||
checkpointSettings: settings,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "updateCheckpointSettings": {
|
||||
if (message.checkpointSettings) {
|
||||
await CheckpointSettingsManager.getInstance().saveSettings(message.checkpointSettings)
|
||||
await this.postMessageToWebview({
|
||||
type: "setCheckpointSettings",
|
||||
checkpointSettings: message.checkpointSettings,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "openCheckpointsIgnore": {
|
||||
await vscode.commands.executeCommand("cline.openCheckpointsIgnore")
|
||||
break
|
||||
}
|
||||
case "confirmDeleteAllCheckpoints": {
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
"This action will delete all checkpoints that have been created by Cline! Deleting all checkpoints will remove the ability to use these features for all historical tasks.",
|
||||
"Delete All",
|
||||
"Cancel",
|
||||
)
|
||||
.then(async (selection) => {
|
||||
if (selection === "Delete All") {
|
||||
try {
|
||||
await deleteAllCheckpoints(this.context.globalStoragePath)
|
||||
vscode.window.showInformationMessage("All checkpoints have been deleted")
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage("Failed to delete checkpoints")
|
||||
console.error("Failed to delete all checkpoints:", error)
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
case "taskCompletionViewChanges": {
|
||||
if (message.number) {
|
||||
await this.cline?.presentMultifileDiff(message.number, true)
|
||||
|
||||
@@ -9,6 +9,7 @@ import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
|
||||
import assert from "node:assert"
|
||||
import { telemetryService } from "./services/telemetry/TelemetryService"
|
||||
import { CheckpointSettingsManager } from "./integrations/checkpoints/CheckpointSettings"
|
||||
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
@@ -32,6 +33,9 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
const sidebarProvider = new ClineProvider(context, outputChannel)
|
||||
|
||||
// Initialize CheckpointSettingsManager
|
||||
CheckpointSettingsManager.initialize(context.globalStorageUri.fsPath)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, sidebarProvider, {
|
||||
webviewOptions: { retainContextWhenHidden: true },
|
||||
@@ -123,6 +127,14 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.openCheckpointsIgnore", async () => {
|
||||
const settingsManager = CheckpointSettingsManager.getInstance()
|
||||
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(settingsManager.checkpointsIgnorePath))
|
||||
await vscode.window.showTextDocument(doc)
|
||||
}),
|
||||
)
|
||||
|
||||
/*
|
||||
We use the text document content provider API to show the left side for diff view by creating a virtual document for the original content. This makes it readonly so users know to edit the right side if they want to keep their changes.
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "fs/promises"
|
||||
import { join } from "path"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { GIT_DISABLED_SUFFIX } from "./CheckpointGitOperations"
|
||||
import { CheckpointSettingsManager } from "./CheckpointSettings"
|
||||
|
||||
/**
|
||||
* CheckpointExclusions Module
|
||||
@@ -42,13 +43,9 @@ interface ExclusionResult {
|
||||
|
||||
/**
|
||||
* Returns the default list of file and directory patterns to exclude from checkpoints.
|
||||
* Combines built-in patterns with workspace-specific LFS patterns.
|
||||
*
|
||||
* @param lfsPatterns - Optional array of Git LFS patterns from workspace
|
||||
* @returns Array of glob patterns to exclude
|
||||
* @todo Make this configurable by the user
|
||||
* These patterns will be written to .checkpointsignore when it's created.
|
||||
*/
|
||||
export const getDefaultExclusions = (lfsPatterns: string[] = []): string[] => [
|
||||
export const getDefaultExclusions = (): string[] => [
|
||||
// Build and Development Artifacts
|
||||
".git/",
|
||||
`.git${GIT_DISABLED_SUFFIX}/`,
|
||||
@@ -74,10 +71,51 @@ export const getDefaultExclusions = (lfsPatterns: string[] = []): string[] => [
|
||||
|
||||
// Log Files
|
||||
...getLogFilePatterns(),
|
||||
|
||||
...lfsPatterns,
|
||||
]
|
||||
|
||||
/**
|
||||
* Writes the combined exclusion patterns to Git's exclude file.
|
||||
* Creates the info directory if it doesn't exist.
|
||||
*
|
||||
* @param gitPath - Path to the .git directory
|
||||
* @param lfsPatterns - Optional array of Git LFS patterns to include
|
||||
*/
|
||||
export const writeExcludesFile = async (gitPath: string, lfsPatterns: string[] = []): Promise<void> => {
|
||||
const excludesPath = join(gitPath, "info", "exclude")
|
||||
await fs.mkdir(join(gitPath, "info"), { recursive: true })
|
||||
|
||||
const settingsManager = CheckpointSettingsManager.getInstance()
|
||||
// Ensure .checkpointsignore exists and load its patterns
|
||||
const ignorePatterns = await settingsManager.getIgnorePatterns()
|
||||
|
||||
// Combine patterns and write to git exclude file
|
||||
const patterns = [...ignorePatterns, ...lfsPatterns]
|
||||
await fs.writeFile(excludesPath, patterns.join("\n"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves Git LFS patterns from the workspace's .gitattributes file.
|
||||
* Returns an empty array if no patterns found or file doesn't exist.
|
||||
*
|
||||
* @param workspacePath - Path to the workspace root
|
||||
* @returns Array of Git LFS patterns found in .gitattributes
|
||||
*/
|
||||
export const getLfsPatterns = async (workspacePath: string): Promise<string[]> => {
|
||||
try {
|
||||
const attributesPath = join(workspacePath, ".gitattributes")
|
||||
if (await fileExistsAtPath(attributesPath)) {
|
||||
const attributesContent = await fs.readFile(attributesPath, "utf8")
|
||||
return attributesContent
|
||||
.split("\n")
|
||||
.filter((line) => line.includes("filter=lfs"))
|
||||
.map((line) => line.split(" ")[0].trim())
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to read .gitattributes:", error)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns patterns for common build and development artifact directories
|
||||
* @returns Array of glob patterns for build artifacts
|
||||
@@ -295,41 +333,3 @@ function getGeospatialPatterns(): string[] {
|
||||
function getLogFilePatterns(): string[] {
|
||||
return ["*.error", "*.log", "*.logs", "*.npm-debug.log*", "*.out", "*.stdout", "yarn-debug.log*", "yarn-error.log*"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the combined exclusion patterns to Git's exclude file.
|
||||
* Creates the info directory if it doesn't exist.
|
||||
*
|
||||
* @param gitPath - Path to the .git directory
|
||||
* @param lfsPatterns - Optional array of Git LFS patterns to include
|
||||
*/
|
||||
export const writeExcludesFile = async (gitPath: string, lfsPatterns: string[] = []): Promise<void> => {
|
||||
const excludesPath = join(gitPath, "info", "exclude")
|
||||
await fs.mkdir(join(gitPath, "info"), { recursive: true })
|
||||
|
||||
const patterns = getDefaultExclusions(lfsPatterns)
|
||||
await fs.writeFile(excludesPath, patterns.join("\n"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves Git LFS patterns from the workspace's .gitattributes file.
|
||||
* Returns an empty array if no patterns found or file doesn't exist.
|
||||
*
|
||||
* @param workspacePath - Path to the workspace root
|
||||
* @returns Array of Git LFS patterns found in .gitattributes
|
||||
*/
|
||||
export const getLfsPatterns = async (workspacePath: string): Promise<string[]> => {
|
||||
try {
|
||||
const attributesPath = join(workspacePath, ".gitattributes")
|
||||
if (await fileExistsAtPath(attributesPath)) {
|
||||
const attributesContent = await fs.readFile(attributesPath, "utf8")
|
||||
return attributesContent
|
||||
.split("\n")
|
||||
.filter((line) => line.includes("filter=lfs"))
|
||||
.map((line) => line.split(" ")[0].trim())
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to read .gitattributes:", error)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { getDefaultExclusions } from "./CheckpointExclusions"
|
||||
import { CheckpointSettings } from "../../shared/Checkpoints"
|
||||
|
||||
/**
|
||||
* CheckpointSettings Module
|
||||
*
|
||||
* Manages user-configurable settings for the Checkpoints system. Key features:
|
||||
*
|
||||
* Settings Management:
|
||||
* - Enable/disable checkpoints functionality
|
||||
*
|
||||
* File Exclusions Management:
|
||||
* - .checkpointsignore file for exclusion patterns
|
||||
* - Default patterns for common file types
|
||||
* - User-customizable pattern list
|
||||
*
|
||||
* Storage Structure:
|
||||
* - Settings stored in globalStorage/settings/cline_checkpoints_settings.json
|
||||
* - Ignore patterns stored in globalStorage/settings/.checkpointsignore
|
||||
*
|
||||
* Integration Points:
|
||||
* - Used by CheckpointTracker for file filtering
|
||||
* - Consumed by CheckpointExclusions for pattern management
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default settings values.
|
||||
* These are used when no settings file exists or when reading fails.
|
||||
*/
|
||||
const DEFAULT_SETTINGS: CheckpointSettings = {
|
||||
enableCheckpoints: true, // Enabled by default
|
||||
}
|
||||
|
||||
/**
|
||||
* CheckpointSettingsManager Class
|
||||
*
|
||||
* Handles all checkpoint settings operations including:
|
||||
* - Reading and writing settings to disk
|
||||
* - Managing .checkpointsignore patterns
|
||||
* - Providing default values when needed
|
||||
*
|
||||
* File Structure:
|
||||
* globalStorage/
|
||||
* settings/
|
||||
* cline_checkpoints_settings.json - Contains enable flag
|
||||
* .checkpointsignore - Contains file exclusion patterns
|
||||
*/
|
||||
export class CheckpointSettingsManager {
|
||||
public readonly settingsDir: string
|
||||
public readonly checkpointSettingsPath: string
|
||||
public readonly checkpointsIgnorePath: string
|
||||
private settings: CheckpointSettings = DEFAULT_SETTINGS
|
||||
private static instance: CheckpointSettingsManager | null = null
|
||||
|
||||
/**
|
||||
* Creates a new CheckpointSettingsManager instance.
|
||||
* Initializes paths for settings and ignore files.
|
||||
*
|
||||
* @param globalStoragePath - VS Code's global storage path for the extension
|
||||
*/
|
||||
private constructor(globalStoragePath: string) {
|
||||
this.settingsDir = path.join(globalStoragePath, "settings")
|
||||
this.checkpointSettingsPath = path.join(this.settingsDir, "cline_checkpoints_settings.json")
|
||||
this.checkpointsIgnorePath = path.join(this.settingsDir, ".checkpointsignore")
|
||||
this.readSettings().then((settings) => {
|
||||
this.settings = settings
|
||||
this.migrateEnableCheckpointsSetting()
|
||||
})
|
||||
this.ensureIgnoreFileExists()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the singleton instance
|
||||
*/
|
||||
public static initialize(globalStoragePath: string): void {
|
||||
if (!CheckpointSettingsManager.instance) {
|
||||
CheckpointSettingsManager.instance = new CheckpointSettingsManager(globalStoragePath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance
|
||||
*/
|
||||
public static getInstance(): CheckpointSettingsManager {
|
||||
if (!CheckpointSettingsManager.instance) {
|
||||
throw new Error("CheckpointSettingsManager not initialized")
|
||||
}
|
||||
return CheckpointSettingsManager.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current checkpoint settings from memory.
|
||||
*
|
||||
* @returns CheckpointSettings Current settings
|
||||
*/
|
||||
getSettings(): CheckpointSettings {
|
||||
return this.settings
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads checkpoint settings from disk.
|
||||
* Merges stored settings with defaults to ensure all fields are present.
|
||||
*
|
||||
* @returns Promise<CheckpointSettings> Settings read from disk, with defaults for any missing values
|
||||
*/
|
||||
private async readSettings(): Promise<CheckpointSettings> {
|
||||
try {
|
||||
if (await fileExistsAtPath(this.checkpointSettingsPath)) {
|
||||
const settingsContent = await fs.readFile(this.checkpointSettingsPath, "utf8")
|
||||
return { ...DEFAULT_SETTINGS, ...JSON.parse(settingsContent) }
|
||||
}
|
||||
// If file doesn't exist, create it with default settings
|
||||
await this.saveSettings(DEFAULT_SETTINGS)
|
||||
} catch (error) {
|
||||
console.error("Error reading checkpoint settings:", error)
|
||||
}
|
||||
return DEFAULT_SETTINGS
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves checkpoint settings to disk and updates in-memory settings.
|
||||
* Creates settings directory if it doesn't exist.
|
||||
* Merges new settings with existing ones.
|
||||
*
|
||||
* @param settings - Partial settings to update
|
||||
*/
|
||||
async saveSettings(settings: Partial<CheckpointSettings>): Promise<void> {
|
||||
// Ensure settings directory exists
|
||||
await fs.mkdir(this.settingsDir, { recursive: true })
|
||||
|
||||
// Merge with current settings
|
||||
const updatedSettings = { ...this.settings, ...settings }
|
||||
this.settings = updatedSettings
|
||||
|
||||
// Save to disk
|
||||
await fs.writeFile(this.checkpointSettingsPath, JSON.stringify(updatedSettings, null, 2))
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves patterns from .checkpointsignore file.
|
||||
* Filters out empty lines and comments.
|
||||
*
|
||||
* @returns Promise<string[]> Array of active ignore patterns
|
||||
*/
|
||||
async getIgnorePatterns(): Promise<string[]> {
|
||||
try {
|
||||
if (await fileExistsAtPath(this.checkpointsIgnorePath)) {
|
||||
const content = await fs.readFile(this.checkpointsIgnorePath, "utf8")
|
||||
return content.split("\n").filter((line) => line.trim() && !line.startsWith("#"))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading .checkpointsignore:", error)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures .checkpointsignore file exists.
|
||||
* Creates it with default patterns if it doesn't exist.
|
||||
*/
|
||||
private async ensureIgnoreFileExists(): Promise<void> {
|
||||
try {
|
||||
await fs.mkdir(this.settingsDir, { recursive: true })
|
||||
|
||||
if (!(await fileExistsAtPath(this.checkpointsIgnorePath))) {
|
||||
await fs.writeFile(this.checkpointsIgnorePath, getDefaultExclusions().join("\n"))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating .checkpointsignore:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the enableCheckpoints setting from VSCode configuration to settings file
|
||||
* All checkpoints settings will be kept in the CheckpointSettingsView from now on
|
||||
*/
|
||||
private async migrateEnableCheckpointsSetting(): Promise<void> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const enableCheckpoints = config.get<boolean>("enableCheckpoints")
|
||||
|
||||
if (enableCheckpoints !== undefined) {
|
||||
// Save to settings file
|
||||
await this.saveSettings({
|
||||
enableCheckpoints,
|
||||
})
|
||||
|
||||
// Remove from VSCode configuration
|
||||
await config.update("enableCheckpoints", undefined, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitializes the settings manager by reading settings from disk.
|
||||
* This should be called when a new CheckpointTracker is created.
|
||||
*/
|
||||
async reinitialize(): Promise<void> {
|
||||
this.settings = await this.readSettings()
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import simpleGit, { SimpleGit } from "simple-git"
|
||||
import simpleGit, { type SimpleGit } from "simple-git"
|
||||
import * as vscode from "vscode"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { GitOperations } from "./CheckpointGitOperations"
|
||||
import { getShadowGitPath, hashWorkingDir, getWorkingDirectory, detectLegacyCheckpoint } from "./CheckpointUtils"
|
||||
import { CheckpointSettingsManager } from "./CheckpointSettings"
|
||||
|
||||
/**
|
||||
* CheckpointTracker Module
|
||||
@@ -85,7 +86,7 @@ class CheckpointTracker {
|
||||
* - Sets up task-specific branch for new checkpoints
|
||||
*
|
||||
* Configuration:
|
||||
* - Respects 'cline.enableCheckpoints' VS Code setting
|
||||
* - Uses settings from CheckpointSettingsManager
|
||||
* - Uses branch-per-task architecture for new checkpoints
|
||||
* - Maintains backwards compatibility with legacy structure
|
||||
*/
|
||||
@@ -96,9 +97,13 @@ class CheckpointTracker {
|
||||
try {
|
||||
console.info(`Creating new CheckpointTracker for task ${taskId}`)
|
||||
|
||||
// Check if checkpoints are disabled in VS Code settings
|
||||
const enableCheckpoints = vscode.workspace.getConfiguration("cline").get<boolean>("enableCheckpoints") ?? true
|
||||
if (!enableCheckpoints) {
|
||||
// Get settings manager instance and reinitialize
|
||||
const settingsManager = CheckpointSettingsManager.getInstance()
|
||||
await settingsManager.reinitialize()
|
||||
|
||||
// Check if checkpoints are enabled in settings
|
||||
const settings = settingsManager.getSettings()
|
||||
if (!settings.enableCheckpoints) {
|
||||
return undefined // Don't create tracker when disabled
|
||||
}
|
||||
|
||||
@@ -291,7 +296,6 @@ class CheckpointTracker {
|
||||
* - the current working directory (including uncommitted changes).
|
||||
*
|
||||
* If `rhsHash` is omitted, compares `lhsHash` to the working directory.
|
||||
* If you want truly untracked files to appear, `git add` them first.
|
||||
*
|
||||
* @param lhsHash - The commit to compare from (older commit)
|
||||
* @param rhsHash - The commit to compare to (newer commit).
|
||||
@@ -339,7 +343,7 @@ class CheckpointTracker {
|
||||
const batchSize = 50
|
||||
|
||||
// Get list of files that exist in base commit
|
||||
const existingFiles = await this.getExistingFiles(git, baseHash, files)
|
||||
const existingFiles = await this.getExistingFiles(git, baseHash)
|
||||
|
||||
// Process files in batches
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
@@ -373,7 +377,7 @@ class CheckpointTracker {
|
||||
let afterContents: string[] = []
|
||||
if (rhsHash) {
|
||||
// Split after files into existing and new in target commit
|
||||
const afterExistingFiles = await this.getExistingFiles(git, rhsHash, batch)
|
||||
const afterExistingFiles = await this.getExistingFiles(git, rhsHash)
|
||||
const afterExistingBatch = batch.filter((file) => afterExistingFiles.has(file))
|
||||
|
||||
if (afterExistingBatch.length > 0) {
|
||||
@@ -438,7 +442,7 @@ class CheckpointTracker {
|
||||
/**
|
||||
* Helper function to get a set of files that exist in a given commit
|
||||
*/
|
||||
private async getExistingFiles(git: SimpleGit, commitHash: string, files: string[]): Promise<Set<string>> {
|
||||
private async getExistingFiles(git: SimpleGit, commitHash: string): Promise<Set<string>> {
|
||||
try {
|
||||
const result = await git.raw(["ls-tree", "-r", "--name-only", commitHash])
|
||||
const existingFiles = new Set<string>(result.split("\n"))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mkdir } from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
|
||||
@@ -26,9 +26,10 @@ export async function getLegacyShadowGitPath(globalStoragePath: string, taskId:
|
||||
throw new Error("Global storage uri is invalid")
|
||||
}
|
||||
const checkpointsDir = path.join(globalStoragePath, "tasks", taskId, "checkpoints")
|
||||
await mkdir(checkpointsDir, { recursive: true })
|
||||
await fs.mkdir(checkpointsDir, { recursive: true })
|
||||
|
||||
const gitPath = path.join(checkpointsDir, ".git")
|
||||
console.info(`Legacy shadow git path: ${gitPath}`)
|
||||
console.log(`Legacy shadow git path: ${gitPath}`)
|
||||
return gitPath
|
||||
}
|
||||
|
||||
@@ -63,7 +64,8 @@ export async function getShadowGitPath(
|
||||
throw new Error("Global storage uri is invalid")
|
||||
}
|
||||
const checkpointsDir = path.join(globalStoragePath, "checkpoints", cwdHash)
|
||||
await mkdir(checkpointsDir, { recursive: true })
|
||||
await fs.mkdir(checkpointsDir, { recursive: true })
|
||||
|
||||
const gitPath = path.join(checkpointsDir, ".git")
|
||||
return gitPath
|
||||
}
|
||||
@@ -154,6 +156,40 @@ export async function detectLegacyCheckpoint(globalStoragePath: string | undefin
|
||||
}
|
||||
const legacyGitPath = path.join(globalStoragePath, "tasks", taskId, "checkpoints", ".git")
|
||||
const isLegacy = await fileExistsAtPath(legacyGitPath)
|
||||
|
||||
console.info(`Legacy checkpoint detection result: ${isLegacy}`)
|
||||
return isLegacy
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all checkpoint data across all tasks.
|
||||
* This is a destructive operation that removes all checkpoint history.
|
||||
* Handles both legacy checkpoints (under tasks/{taskId}/checkpoints/.git/)
|
||||
* and branch-per-task checkpoints (under checkpoints/{workspaceHash}/.git/).
|
||||
*
|
||||
* @param globalStoragePath - The VS Code global storage path
|
||||
* @throws Error if deletion fails or if global storage path is invalid
|
||||
*/
|
||||
export async function deleteAllCheckpoints(globalStoragePath: string): Promise<void> {
|
||||
if (!globalStoragePath) {
|
||||
throw new Error("Global storage path is invalid")
|
||||
}
|
||||
|
||||
// Delete legacy checkpoints
|
||||
const tasksDir = path.join(globalStoragePath, "tasks")
|
||||
if (await fileExistsAtPath(tasksDir)) {
|
||||
const taskDirs = await fs.readdir(tasksDir)
|
||||
for (const taskId of taskDirs) {
|
||||
const checkpointsDir = path.join(tasksDir, taskId, "checkpoints")
|
||||
if (await fileExistsAtPath(checkpointsDir)) {
|
||||
await fs.rm(checkpointsDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete branch-per-task checkpoints
|
||||
const checkpointsDir = path.join(globalStoragePath, "checkpoints")
|
||||
if (await fileExistsAtPath(checkpointsDir)) {
|
||||
await fs.rm(checkpointsDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface CheckpointSettings {
|
||||
/** Whether checkpoints are enabled */
|
||||
enableCheckpoints: boolean
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { ChatSettings } from "./ChatSettings"
|
||||
import { HistoryItem } from "./HistoryItem"
|
||||
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
|
||||
import { TelemetrySetting } from "./TelemetrySetting"
|
||||
import { CheckpointSettings } from "./Checkpoints"
|
||||
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
@@ -33,6 +34,7 @@ export interface ExtensionMessage {
|
||||
| "commitSearchResults"
|
||||
| "openGraphData"
|
||||
| "isImageUrlResult"
|
||||
| "setCheckpointSettings"
|
||||
text?: string
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
@@ -56,6 +58,7 @@ export interface ExtensionMessage {
|
||||
mcpMarketplaceCatalog?: McpMarketplaceCatalog
|
||||
error?: string
|
||||
mcpDownloadDetails?: McpDownloadResponse
|
||||
checkpointSettings?: CheckpointSettings
|
||||
commits?: GitCommit[]
|
||||
openGraphData?: {
|
||||
title?: string
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { ChatContent } from "./ChatContent"
|
||||
import { CheckpointSettings } from "./Checkpoints"
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
@@ -36,6 +37,10 @@ export interface WebviewMessage {
|
||||
| "togglePlanActMode"
|
||||
| "checkpointDiff"
|
||||
| "checkpointRestore"
|
||||
| "getCheckpointSettings"
|
||||
| "updateCheckpointSettings"
|
||||
| "openCheckpointsIgnore"
|
||||
| "confirmDeleteAllCheckpoints"
|
||||
| "taskCompletionViewChanges"
|
||||
| "openExtensionSettings"
|
||||
| "requestVsCodeLmModels"
|
||||
@@ -67,6 +72,7 @@ export interface WebviewMessage {
|
||||
number?: number
|
||||
autoApprovalSettings?: AutoApprovalSettings
|
||||
browserSettings?: BrowserSettings
|
||||
checkpointSettings?: CheckpointSettings
|
||||
chatSettings?: ChatSettings
|
||||
chatContent?: ChatContent
|
||||
mcpId?: string
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { VSCodeButton, VSCodeLink, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useEffect, useState } from "react"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CheckpointSettings } from "../../../../src/shared/Checkpoints"
|
||||
|
||||
const CheckpointsSettingsView = () => {
|
||||
const [settings, setSettings] = useState<CheckpointSettings>({
|
||||
enableCheckpoints: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Message extension to get initial settings
|
||||
vscode.postMessage({
|
||||
type: "getCheckpointSettings",
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
switch (message.type) {
|
||||
case "setCheckpointSettings": {
|
||||
const newSettings = message.checkpointSettings
|
||||
setSettings(newSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", messageHandler)
|
||||
return () => window.removeEventListener("message", messageHandler)
|
||||
}, [])
|
||||
|
||||
const handleEnableChange = (e: any) => {
|
||||
const checkbox = e.target as HTMLInputElement
|
||||
const newSettings = {
|
||||
...settings,
|
||||
enableCheckpoints: checkbox.checked,
|
||||
}
|
||||
setSettings(newSettings)
|
||||
vscode.postMessage({
|
||||
type: "updateCheckpointSettings",
|
||||
checkpointSettings: newSettings,
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteCheckpoints = () => {
|
||||
vscode.postMessage({
|
||||
type: "confirmDeleteAllCheckpoints",
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
||||
<div>
|
||||
<div style={{ fontWeight: "500", marginBottom: "8px" }}>Enable Checkpoints</div>
|
||||
<VSCodeCheckbox checked={settings.enableCheckpoints} onChange={handleEnableChange}>
|
||||
Enable checkpoints
|
||||
</VSCodeCheckbox>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Automatically create checkpoints when Cline makes changes to your files
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: "500", marginBottom: "8px" }}>Checkpoints File Exclusions</div>
|
||||
<VSCodeLink onClick={() => vscode.postMessage({ type: "openCheckpointsIgnore" })}>
|
||||
Edit .checkpointsignore
|
||||
</VSCodeLink>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Configure which files and directories to exclude from checkpoint creation globally
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style={{ fontWeight: "500", marginBottom: "8px" }}>Delete All Checkpoints</div>
|
||||
<VSCodeButton onClick={handleDeleteCheckpoints}>
|
||||
<i className="codicon codicon-trash" style={{ marginRight: "6px" }} />
|
||||
Delete All
|
||||
</VSCodeButton>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Permanently remove all saved checkpoints from your system
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(CheckpointsSettingsView)
|
||||
@@ -3,14 +3,18 @@ import { memo, useEffect, useState } from "react"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import SettingsButton from "../common/SettingsButton"
|
||||
import ApiOptions from "./ApiOptions"
|
||||
import SettingsButton from "../common/SettingsButton"
|
||||
import CheckpointsSettingsView from "./CheckpointSettingsView"
|
||||
|
||||
const { IS_DEV } = process.env
|
||||
|
||||
type SettingsViewProps = {
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
type View = "main" | "checkpoints"
|
||||
|
||||
const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
const {
|
||||
apiConfiguration,
|
||||
@@ -23,6 +27,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
} = useExtensionState()
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [currentView, setCurrentView] = useState<View>("main")
|
||||
|
||||
const handleSubmit = () => {
|
||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||
@@ -66,6 +71,45 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
vscode.postMessage({ type: "resetState" })
|
||||
}
|
||||
|
||||
if (currentView === "checkpoints") {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
padding: "10px 0px 0px 20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "17px",
|
||||
paddingRight: 17,
|
||||
}}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Checkpoint Settings</h3>
|
||||
<VSCodeButton onClick={() => setCurrentView("main")}>Done</VSCodeButton>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
overflowY: "scroll",
|
||||
paddingRight: 8,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
<CheckpointsSettingsView />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -126,6 +170,14 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<div style={{ fontWeight: "500", marginBottom: "8px" }}>Checkpoints</div>
|
||||
<SettingsButton onClick={() => setCurrentView("checkpoints")}>
|
||||
<i className="codicon codicon-bookmark" />
|
||||
Configure Checkpoints
|
||||
</SettingsButton>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<VSCodeCheckbox
|
||||
style={{ marginBottom: "5px" }}
|
||||
|
||||
Reference in New Issue
Block a user