From 4c1a8c3b50cccac3040a483564364a0cd438b0fa Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 16 Dec 2025 09:56:48 +0000 Subject: [PATCH] feat: add auto-approve during todo execution setting - Add new setting alwaysAllowDuringTodoExecution to GlobalSettings - Update auto-approval logic to automatically approve write operations while todos are in progress (pending or in_progress status) - Add UI toggle for the new setting in Settings and AutoApproveDropdown - Add comprehensive tests for the new auto-approval behavior This addresses the request to allow automatic continuation of modifications while a todo list is being executed, reducing the need for manual approval during task completion. Closes #10123 --- packages/types/src/global-settings.ts | 2 + .../__tests__/checkAutoApproval.spec.ts | 178 ++++++++++++++++++ src/core/auto-approval/index.ts | 29 ++- src/core/task/Task.ts | 2 +- src/shared/ExtensionMessage.ts | 1 + .../components/chat/AutoApproveDropdown.tsx | 5 + .../settings/AutoApproveSettings.tsx | 4 + .../components/settings/AutoApproveToggle.tsx | 8 + .../src/components/settings/SettingsView.tsx | 3 + .../__tests__/AutoApproveToggle.spec.tsx | 1 + .../src/context/ExtensionStateContext.tsx | 3 + .../src/hooks/useAutoApprovalToggles.ts | 3 + webview-ui/src/i18n/locales/en/settings.json | 4 + 13 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 src/core/auto-approval/__tests__/checkAutoApproval.spec.ts diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index a11fae1e11..96174e9de3 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -80,6 +80,7 @@ export const globalSettingsSchema = z.object({ alwaysAllowSubtasks: z.boolean().optional(), alwaysAllowExecute: z.boolean().optional(), alwaysAllowFollowupQuestions: z.boolean().optional(), + alwaysAllowDuringTodoExecution: z.boolean().optional(), followupAutoApproveTimeoutMs: z.number().optional(), allowedCommands: z.array(z.string()).optional(), deniedCommands: z.array(z.string()).optional(), @@ -311,6 +312,7 @@ export const EVALS_SETTINGS: RooCodeSettings = { alwaysAllowSubtasks: true, alwaysAllowExecute: true, alwaysAllowFollowupQuestions: true, + alwaysAllowDuringTodoExecution: false, followupAutoApproveTimeoutMs: 0, allowedCommands: ["*"], commandExecutionTimeout: 20, diff --git a/src/core/auto-approval/__tests__/checkAutoApproval.spec.ts b/src/core/auto-approval/__tests__/checkAutoApproval.spec.ts new file mode 100644 index 0000000000..a1dfa1cc3d --- /dev/null +++ b/src/core/auto-approval/__tests__/checkAutoApproval.spec.ts @@ -0,0 +1,178 @@ +import { checkAutoApproval } from "../index" +import type { TodoItem } from "@roo-code/types" + +describe("checkAutoApproval", () => { + describe("alwaysAllowDuringTodoExecution", () => { + const baseState = { + autoApprovalEnabled: true, + alwaysAllowDuringTodoExecution: true, + alwaysAllowWrite: false, // Intentionally false to test that todo execution takes precedence + } + + // Tool names used in auto-approval are: editedExistingFile, appliedDiff, newFileCreated + const writeToolText = JSON.stringify({ + tool: "editedExistingFile", + path: "test.ts", + content: "test content", + }) + + it("should auto-approve write operations when todos are in progress", async () => { + const todoList: TodoItem[] = [ + { id: "1", content: "Task 1", status: "completed" }, + { id: "2", content: "Task 2", status: "in_progress" }, + { id: "3", content: "Task 3", status: "pending" }, + ] + + const result = await checkAutoApproval({ + state: baseState, + ask: "tool", + text: writeToolText, + todoList, + }) + + expect(result.decision).toBe("approve") + }) + + it("should auto-approve when todos are pending", async () => { + const todoList: TodoItem[] = [{ id: "1", content: "Task 1", status: "pending" }] + + const result = await checkAutoApproval({ + state: baseState, + ask: "tool", + text: writeToolText, + todoList, + }) + + expect(result.decision).toBe("approve") + }) + + it("should not auto-approve when all todos are completed", async () => { + const todoList: TodoItem[] = [ + { id: "1", content: "Task 1", status: "completed" }, + { id: "2", content: "Task 2", status: "completed" }, + ] + + const result = await checkAutoApproval({ + state: baseState, + ask: "tool", + text: writeToolText, + todoList, + }) + + // Should fall back to normal behavior - since alwaysAllowWrite is false, should ask + expect(result.decision).toBe("ask") + }) + + it("should not auto-approve when todo list is empty", async () => { + const result = await checkAutoApproval({ + state: baseState, + ask: "tool", + text: writeToolText, + todoList: [], + }) + + expect(result.decision).toBe("ask") + }) + + it("should not auto-approve when todo list is undefined", async () => { + const result = await checkAutoApproval({ + state: baseState, + ask: "tool", + text: writeToolText, + todoList: undefined, + }) + + expect(result.decision).toBe("ask") + }) + + it("should not auto-approve when alwaysAllowDuringTodoExecution is false", async () => { + const state = { + ...baseState, + alwaysAllowDuringTodoExecution: false, + } + + const todoList: TodoItem[] = [{ id: "1", content: "Task 1", status: "in_progress" }] + + const result = await checkAutoApproval({ + state, + ask: "tool", + text: writeToolText, + todoList, + }) + + expect(result.decision).toBe("ask") + }) + + it("should not auto-approve protected files during todo execution", async () => { + const todoList: TodoItem[] = [{ id: "1", content: "Task 1", status: "in_progress" }] + + const result = await checkAutoApproval({ + state: baseState, + ask: "tool", + text: writeToolText, + isProtected: true, + todoList, + }) + + expect(result.decision).toBe("ask") + }) + + it("should not auto-approve files outside workspace during todo execution", async () => { + const todoList: TodoItem[] = [{ id: "1", content: "Task 1", status: "in_progress" }] + + const outsideWorkspaceWriteToolText = JSON.stringify({ + tool: "editedExistingFile", + path: "/outside/workspace/test.ts", + content: "test content", + isOutsideWorkspace: true, + }) + + const result = await checkAutoApproval({ + state: baseState, + ask: "tool", + text: outsideWorkspaceWriteToolText, + todoList, + }) + + expect(result.decision).toBe("ask") + }) + + it("should fall back to regular alwaysAllowWrite when todos are complete", async () => { + const state = { + ...baseState, + alwaysAllowWrite: true, // Enable regular write auto-approval + } + + const todoList: TodoItem[] = [{ id: "1", content: "Task 1", status: "completed" }] + + const result = await checkAutoApproval({ + state, + ask: "tool", + text: writeToolText, + todoList, + }) + + // Should fall back to alwaysAllowWrite behavior + expect(result.decision).toBe("approve") + }) + + it("should auto-approve appliedDiff tool during todo execution", async () => { + const todoList: TodoItem[] = [{ id: "1", content: "Task 1", status: "in_progress" }] + + const applyDiffToolText = JSON.stringify({ + tool: "appliedDiff", + path: "test.ts", + diff: "some diff content", + }) + + const result = await checkAutoApproval({ + state: baseState, + ask: "tool", + text: applyDiffToolText, + todoList, + }) + + expect(result.decision).toBe("approve") + }) + }) +}) diff --git a/src/core/auto-approval/index.ts b/src/core/auto-approval/index.ts index da099d6aeb..ef2cea4a39 100644 --- a/src/core/auto-approval/index.ts +++ b/src/core/auto-approval/index.ts @@ -1,4 +1,4 @@ -import { type ClineAsk, type McpServerUse, type FollowUpData, isNonBlockingAsk } from "@roo-code/types" +import { type ClineAsk, type McpServerUse, type FollowUpData, type TodoItem, isNonBlockingAsk } from "@roo-code/types" import type { ClineSayTool, ExtensionState } from "../../shared/ExtensionMessage" import { ClineAskResponse } from "../../shared/WebviewMessage" @@ -17,6 +17,7 @@ export type AutoApprovalState = | "alwaysAllowSubtasks" | "alwaysAllowExecute" | "alwaysAllowFollowupQuestions" + | "alwaysAllowDuringTodoExecution" // Some of these actions have additional settings associated with them. export type AutoApprovalStateOptions = @@ -29,6 +30,17 @@ export type AutoApprovalStateOptions = | "allowedCommands" // For `alwaysAllowExecute`. | "deniedCommands" +/** + * Check if there are incomplete todo items in the list. + * A todo is considered incomplete if its status is "pending" or "in_progress". + */ +function hasTodosInProgress(todoList?: TodoItem[]): boolean { + if (!todoList || todoList.length === 0) { + return false + } + return todoList.some((todo) => todo.status === "pending" || todo.status === "in_progress") +} + export type CheckAutoApprovalResult = | { decision: "approve" } | { decision: "deny" } @@ -44,11 +56,13 @@ export async function checkAutoApproval({ ask, text, isProtected, + todoList, }: { state?: Pick ask: ClineAsk text?: string isProtected?: boolean + todoList?: TodoItem[] }): Promise { if (isNonBlockingAsk(ask)) { return { decision: "approve" } @@ -173,6 +187,19 @@ export async function checkAutoApproval({ } if (isWriteToolAction(tool)) { + // Check if auto-approve during todo execution is enabled and todos are in progress + const todosInProgress = hasTodosInProgress(todoList) + const autoApproveDuringTodo = + state.alwaysAllowDuringTodoExecution === true && + todosInProgress && + !isOutsideWorkspace && // Never auto-approve outside workspace during todo execution + !isProtected // Never auto-approve protected files during todo execution + + if (autoApproveDuringTodo) { + return { decision: "approve" } + } + + // Fall back to regular write auto-approval logic return state.alwaysAllowWrite === true && (!isOutsideWorkspace || state.alwaysAllowWriteOutsideWorkspace === true) && (!isProtected || state.alwaysAllowWriteProtected === true) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index b515e60855..6dacdca1f5 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1089,7 +1089,7 @@ export class Task extends EventEmitter implements TaskLike { // Automatically approve if the ask according to the user's settings. const provider = this.providerRef.deref() const state = provider ? await provider.getState() : undefined - const approval = await checkAutoApproval({ state, ask: type, text, isProtected }) + const approval = await checkAutoApproval({ state, ask: type, text, isProtected, todoList: this.todoList }) if (approval.decision === "approve") { this.approveAsk() diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 93528b8d56..4d09813ef7 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -238,6 +238,7 @@ export type ExtensionState = Pick< | "alwaysAllowSubtasks" | "alwaysAllowFollowupQuestions" | "alwaysAllowExecute" + | "alwaysAllowDuringTodoExecution" | "followupAutoApproveTimeoutMs" | "allowedCommands" | "deniedCommands" diff --git a/webview-ui/src/components/chat/AutoApproveDropdown.tsx b/webview-ui/src/components/chat/AutoApproveDropdown.tsx index 857eb5cfb1..eba37c1f5b 100644 --- a/webview-ui/src/components/chat/AutoApproveDropdown.tsx +++ b/webview-ui/src/components/chat/AutoApproveDropdown.tsx @@ -39,6 +39,7 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }: setAlwaysAllowModeSwitch, setAlwaysAllowSubtasks, setAlwaysAllowFollowupQuestions, + setAlwaysAllowDuringTodoExecution, } = useExtensionState() const toggles = useAutoApprovalToggles() @@ -72,6 +73,9 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }: case "alwaysAllowFollowupQuestions": setAlwaysAllowFollowupQuestions(value) break + case "alwaysAllowDuringTodoExecution": + setAlwaysAllowDuringTodoExecution(value) + break } // If enabling any option, ensure autoApprovalEnabled is true. @@ -90,6 +94,7 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }: setAlwaysAllowModeSwitch, setAlwaysAllowSubtasks, setAlwaysAllowFollowupQuestions, + setAlwaysAllowDuringTodoExecution, setAutoApprovalEnabled, ], ) diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index 335a616a3d..c62788ec1f 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -29,6 +29,7 @@ type AutoApproveSettingsProps = HTMLAttributes & { alwaysAllowSubtasks?: boolean alwaysAllowExecute?: boolean alwaysAllowFollowupQuestions?: boolean + alwaysAllowDuringTodoExecution?: boolean followupAutoApproveTimeoutMs?: number allowedCommands?: string[] allowedMaxRequests?: number | undefined @@ -46,6 +47,7 @@ type AutoApproveSettingsProps = HTMLAttributes & { | "alwaysAllowSubtasks" | "alwaysAllowExecute" | "alwaysAllowFollowupQuestions" + | "alwaysAllowDuringTodoExecution" | "followupAutoApproveTimeoutMs" | "allowedCommands" | "allowedMaxRequests" @@ -66,6 +68,7 @@ export const AutoApproveSettings = ({ alwaysAllowSubtasks, alwaysAllowExecute, alwaysAllowFollowupQuestions, + alwaysAllowDuringTodoExecution, followupAutoApproveTimeoutMs = 60000, allowedCommands, allowedMaxRequests, @@ -160,6 +163,7 @@ export const AutoApproveSettings = ({ alwaysAllowSubtasks={alwaysAllowSubtasks} alwaysAllowExecute={alwaysAllowExecute} alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions} + alwaysAllowDuringTodoExecution={alwaysAllowDuringTodoExecution} onToggle={(key, value) => setCachedStateField(key, value)} /> diff --git a/webview-ui/src/components/settings/AutoApproveToggle.tsx b/webview-ui/src/components/settings/AutoApproveToggle.tsx index 2ddfdfd972..86877e4053 100644 --- a/webview-ui/src/components/settings/AutoApproveToggle.tsx +++ b/webview-ui/src/components/settings/AutoApproveToggle.tsx @@ -14,6 +14,7 @@ type AutoApproveToggles = Pick< | "alwaysAllowSubtasks" | "alwaysAllowExecute" | "alwaysAllowFollowupQuestions" + | "alwaysAllowDuringTodoExecution" > export type AutoApproveSetting = keyof AutoApproveToggles @@ -83,6 +84,13 @@ export const autoApproveSettingsConfig: Record(({ onDone, t customSupportPrompts, profileThresholds, alwaysAllowFollowupQuestions, + alwaysAllowDuringTodoExecution, followupAutoApproveTimeoutMs, includeDiagnosticMessages, maxDiagnosticMessages, @@ -404,6 +405,7 @@ const SettingsView = forwardRef(({ onDone, t maxDiagnosticMessages: maxDiagnosticMessages ?? 50, alwaysAllowSubtasks, alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, + alwaysAllowDuringTodoExecution: alwaysAllowDuringTodoExecution ?? false, followupAutoApproveTimeoutMs, condensingApiConfigId: condensingApiConfigId || "", includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true, @@ -718,6 +720,7 @@ const SettingsView = forwardRef(({ onDone, t alwaysAllowSubtasks={alwaysAllowSubtasks} alwaysAllowExecute={alwaysAllowExecute} alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions} + alwaysAllowDuringTodoExecution={alwaysAllowDuringTodoExecution} followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs} allowedCommands={allowedCommands} allowedMaxRequests={allowedMaxRequests ?? undefined} diff --git a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx index b67781f8da..2f15dea070 100644 --- a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx @@ -25,6 +25,7 @@ describe("AutoApproveToggle", () => { alwaysAllowSubtasks: false, alwaysAllowExecute: true, alwaysAllowFollowupQuestions: false, + alwaysAllowDuringTodoExecution: false, onToggle: mockOnToggle, } diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 08294f9fe8..de7f53ceb1 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -71,6 +71,7 @@ export interface ExtensionStateContextType extends ExtensionState { setAlwaysAllowMcp: (value: boolean) => void setAlwaysAllowModeSwitch: (value: boolean) => void setAlwaysAllowSubtasks: (value: boolean) => void + setAlwaysAllowDuringTodoExecution: (value: boolean) => void setBrowserToolEnabled: (value: boolean) => void setShowRooIgnoredFiles: (value: boolean) => void setShowAnnouncement: (value: boolean) => void @@ -488,6 +489,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })), setAlwaysAllowModeSwitch: (value) => setState((prevState) => ({ ...prevState, alwaysAllowModeSwitch: value })), setAlwaysAllowSubtasks: (value) => setState((prevState) => ({ ...prevState, alwaysAllowSubtasks: value })), + setAlwaysAllowDuringTodoExecution: (value) => + setState((prevState) => ({ ...prevState, alwaysAllowDuringTodoExecution: value })), setAlwaysAllowFollowupQuestions, setFollowupAutoApproveTimeoutMs: (value) => setState((prevState) => ({ ...prevState, followupAutoApproveTimeoutMs: value })), diff --git a/webview-ui/src/hooks/useAutoApprovalToggles.ts b/webview-ui/src/hooks/useAutoApprovalToggles.ts index 9c3e1c689a..e4f5cfb34e 100644 --- a/webview-ui/src/hooks/useAutoApprovalToggles.ts +++ b/webview-ui/src/hooks/useAutoApprovalToggles.ts @@ -15,6 +15,7 @@ export function useAutoApprovalToggles() { alwaysAllowModeSwitch, alwaysAllowSubtasks, alwaysAllowFollowupQuestions, + alwaysAllowDuringTodoExecution, } = useExtensionState() const toggles = useMemo( @@ -27,6 +28,7 @@ export function useAutoApprovalToggles() { alwaysAllowModeSwitch, alwaysAllowSubtasks, alwaysAllowFollowupQuestions, + alwaysAllowDuringTodoExecution, }), [ alwaysAllowReadOnly, @@ -37,6 +39,7 @@ export function useAutoApprovalToggles() { alwaysAllowModeSwitch, alwaysAllowSubtasks, alwaysAllowFollowupQuestions, + alwaysAllowDuringTodoExecution, ], ) diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 56488602b4..d152abad72 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -227,6 +227,10 @@ "description": "Automatically select the first suggested answer for follow-up questions after the configured timeout", "timeoutLabel": "Time to wait before auto-selecting the first answer" }, + "todoExecution": { + "label": "Todo", + "description": "Automatically approve file writes while a todo list is being executed. Helps Roo complete todo items without interruption." + }, "execute": { "label": "Execute", "description": "Automatically execute allowed terminal commands without requiring approval",