feat: add deferFileApprovalToCompletion setting for batch file approval

- Add new global setting deferFileApprovalToCompletion
- Auto-approve file write operations when setting is enabled
- Show Keep All Changes / Undo All Changes buttons at task completion
- Restore initial checkpoint when user chooses to undo
- Add i18n translations for new buttons

Addresses Issue #10123
This commit is contained in:
Roo Code 2025-12-16 09:54:42 +00:00
parent 596783d365
commit 7b5b40e784
9 changed files with 106 additions and 7 deletions

View file

@ -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(),

View file

@ -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" }
}
}

View file

@ -4089,6 +4089,18 @@ export class Task extends EventEmitter<TaskEvents> 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[]) {

View file

@ -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

View file

@ -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.",

View file

@ -243,6 +243,7 @@ export type ExtensionState = Pick<
| "deniedCommands"
| "allowedMaxRequests"
| "allowedMaxCost"
| "deferFileApprovalToCompletion"
| "browserToolEnabled"
| "browserViewportSize"
| "screenshotQuality"

View file

@ -176,6 +176,8 @@ export interface WebviewMessage {
| "browserPanelDidLaunch"
| "openDebugApiHistory"
| "openDebugUiHistory"
| "keepAllChanges"
| "undoAllChanges"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"

View file

@ -96,6 +96,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
cloudIsAuthenticated,
messageQueue = [],
isBrowserSessionActive,
deferFileApprovalToCompletion,
} = useExtensionState()
const messagesRef = useRef(messages)
@ -360,8 +361,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setSendingDisabled(isPartial)
setClineAsk("completion_result")
setEnableButtons(!isPartial)
setPrimaryButtonText(t("chat:startNewTask.title"))
setSecondaryButtonText(undefined)
// When deferFileApprovalToCompletion is enabled, show Keep/Undo buttons
if (deferFileApprovalToCompletion) {
setPrimaryButtonText(t("chat:keepAllChanges.title"))
setSecondaryButtonText(t("chat:undoAllChanges.title"))
} else {
setPrimaryButtonText(t("chat:startNewTask.title"))
setSecondaryButtonText(undefined)
}
break
case "resume_task":
setSendingDisabled(false)
@ -722,6 +729,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}
break
case "completion_result":
// When deferFileApprovalToCompletion is enabled, primary button is "Keep All Changes"
// which keeps the changes and starts a new task
if (deferFileApprovalToCompletion) {
// Notify extension to keep all changes (don't restore checkpoint)
vscode.postMessage({ type: "keepAllChanges" })
}
startNewTask()
break
case "resume_completed_task":
// Waiting for feedback, but we can just present a new task button
startNewTask()
@ -735,7 +750,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setClineAsk(undefined)
setEnableButtons(false)
},
[clineAsk, startNewTask, currentTaskItem?.parentTaskId],
[clineAsk, startNewTask, currentTaskItem?.parentTaskId, deferFileApprovalToCompletion],
)
const handleSecondaryButtonClick = useCallback(
@ -757,6 +772,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
case "resume_task":
startNewTask()
break
case "completion_result":
// When deferFileApprovalToCompletion is enabled, secondary button is "Undo All Changes"
// which restores the checkpoint and starts a new task
if (deferFileApprovalToCompletion) {
// Notify extension to undo all changes (restore to initial checkpoint)
vscode.postMessage({ type: "undoAllChanges" })
}
startNewTask()
break
case "command":
case "tool":
case "browser_action_launch":
@ -785,7 +809,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setClineAsk(undefined)
setEnableButtons(false)
},
[clineAsk, startNewTask, isStreaming],
[clineAsk, startNewTask, isStreaming, deferFileApprovalToCompletion],
)
const { info: model } = useSelectedModel(apiConfiguration)

View file

@ -90,6 +90,14 @@
"title": "Terminate",
"tooltip": "End the current task"
},
"keepAllChanges": {
"title": "Keep All Changes",
"tooltip": "Keep all file changes and start a new task"
},
"undoAllChanges": {
"title": "Undo All Changes",
"tooltip": "Restore files to initial state and start a new task"
},
"cancel": {
"title": "Cancel",
"tooltip": "Cancel the current operation"