diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index a11fae1e11..4a8b76442b 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -86,6 +86,13 @@ export const globalSettingsSchema = z.object({ commandExecutionTimeout: z.number().optional(), commandTimeoutAllowlist: z.array(z.string()).optional(), preventCompletionWithOpenTodos: z.boolean().optional(), + /** + * When enabled, file write operations are auto-approved during task execution. + * At task completion, the user is presented with "Keep All Changes" / "Undo All Changes" options. + * This allows users to see the full result before deciding to keep or revert file changes. + * @default false + */ + deferFileApprovalToCompletion: z.boolean().optional(), allowedMaxRequests: z.number().nullish(), allowedMaxCost: z.number().nullish(), autoCondenseContext: z.boolean().optional(), diff --git a/src/core/auto-approval/index.ts b/src/core/auto-approval/index.ts index da099d6aeb..89e3bfa99e 100644 --- a/src/core/auto-approval/index.ts +++ b/src/core/auto-approval/index.ts @@ -24,6 +24,7 @@ export type AutoApprovalStateOptions = | "alwaysAllowReadOnlyOutsideWorkspace" // For `alwaysAllowReadOnly`. | "alwaysAllowWriteOutsideWorkspace" // For `alwaysAllowWrite`. | "alwaysAllowWriteProtected" + | "deferFileApprovalToCompletion" // Auto-approve file writes, let user undo at completion. | "followupAutoApproveTimeoutMs" // For `alwaysAllowFollowupQuestions`. | "mcpServers" // For `alwaysAllowMcp`. | "allowedCommands" // For `alwaysAllowExecute`. @@ -173,11 +174,20 @@ export async function checkAutoApproval({ } if (isWriteToolAction(tool)) { - return state.alwaysAllowWrite === true && + // When deferFileApprovalToCompletion is enabled, auto-approve file writes + // (except for protected files and files outside workspace, unless explicitly allowed) + // The user can undo all changes at task completion + const isDeferredApproval = + state.deferFileApprovalToCompletion === true && (!isOutsideWorkspace || state.alwaysAllowWriteOutsideWorkspace === true) && (!isProtected || state.alwaysAllowWriteProtected === true) - ? { decision: "approve" } - : { decision: "ask" } + + const isNormalApproval = + state.alwaysAllowWrite === true && + (!isOutsideWorkspace || state.alwaysAllowWriteOutsideWorkspace === true) && + (!isProtected || state.alwaysAllowWriteProtected === true) + + return isDeferredApproval || isNormalApproval ? { decision: "approve" } : { decision: "ask" } } } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index b515e60855..1c3307ec4e 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -4089,6 +4089,18 @@ export class Task extends EventEmitter implements TaskLike { return checkpointDiff(this, options) } + /** + * Get the initial checkpoint hash (first checkpoint saved for this task). + * This is used for "undo all changes" functionality when deferFileApprovalToCompletion is enabled. + * + * @returns The hash of the first checkpoint, or undefined if no checkpoints exist + */ + public getInitialCheckpointHash(): string | undefined { + // Find the first checkpoint_saved message in clineMessages + const firstCheckpoint = this.clineMessages.find((msg) => msg.say === "checkpoint_saved") + return firstCheckpoint?.text + } + // Metrics public combineMessages(messages: ClineMessage[]) { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c08504c576..097ef42ad4 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1104,6 +1104,39 @@ export const webviewMessageHandler = async ( break } + case "keepAllChanges": + // User chose to keep all changes at task completion - nothing to do + // The changes were already applied during task execution + provider.log("User chose to keep all changes at task completion") + break + case "undoAllChanges": { + // User chose to undo all changes at task completion + // Restore to the initial checkpoint from the beginning of the task + provider.log("User chose to undo all changes at task completion") + const currentCline = provider.getCurrentTask() + if (currentCline?.checkpointService) { + try { + // Get the initial checkpoint hash - the first checkpoint saved for this task + const initialCheckpointHash = currentCline.getInitialCheckpointHash?.() + if (initialCheckpointHash) { + await currentCline.checkpointService.restoreCheckpoint(initialCheckpointHash) + provider.log(`Restored to initial checkpoint: ${initialCheckpointHash}`) + } else { + provider.log("No initial checkpoint hash available for restoration") + vscode.window.showWarningMessage(t("common:errors.no_initial_checkpoint")) + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + provider.log(`Failed to restore initial checkpoint: ${errorMessage}`) + vscode.window.showErrorMessage( + t("common:errors.checkpoint_restore_failed", { error: errorMessage }), + ) + } + } else { + provider.log("No checkpoint service available for restoration") + } + break + } case "cancelTask": await provider.cancelTask() break diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index ae2e20a292..54f9d9f20f 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -28,6 +28,8 @@ "could_not_open_file_generic": "Could not open file!", "checkpoint_timeout": "Timed out when attempting to restore checkpoint.", "checkpoint_failed": "Failed to restore checkpoint.", + "checkpoint_restore_failed": "Failed to restore initial checkpoint: {{error}}", + "no_initial_checkpoint": "No initial checkpoint available. Unable to undo changes.", "git_not_installed": "Git is required for the checkpoints feature. Please install Git to enable checkpoints.", "checkpoint_no_first": "No first checkpoint to compare.", "checkpoint_no_previous": "No previous checkpoint to compare.", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 93528b8d56..ebd9d49a05 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -243,6 +243,7 @@ export type ExtensionState = Pick< | "deniedCommands" | "allowedMaxRequests" | "allowedMaxCost" + | "deferFileApprovalToCompletion" | "browserToolEnabled" | "browserViewportSize" | "screenshotQuality" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index eb109166c8..62140d4baa 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -176,6 +176,8 @@ export interface WebviewMessage { | "browserPanelDidLaunch" | "openDebugApiHistory" | "openDebugUiHistory" + | "keepAllChanges" + | "undoAllChanges" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index a002abf8cf..4db8ed8d4f 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -96,6 +96,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction