mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-08 22:21:23 +00:00
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
This commit is contained in:
parent
596783d365
commit
4c1a8c3b50
13 changed files with 241 additions and 2 deletions
|
|
@ -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,
|
||||
|
|
|
|||
178
src/core/auto-approval/__tests__/checkAutoApproval.spec.ts
Normal file
178
src/core/auto-approval/__tests__/checkAutoApproval.spec.ts
Normal file
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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<ExtensionState, AutoApprovalState | AutoApprovalStateOptions>
|
||||
ask: ClineAsk
|
||||
text?: string
|
||||
isProtected?: boolean
|
||||
todoList?: TodoItem[]
|
||||
}): Promise<CheckAutoApprovalResult> {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1089,7 +1089,7 @@ export class Task extends EventEmitter<TaskEvents> 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()
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ export type ExtensionState = Pick<
|
|||
| "alwaysAllowSubtasks"
|
||||
| "alwaysAllowFollowupQuestions"
|
||||
| "alwaysAllowExecute"
|
||||
| "alwaysAllowDuringTodoExecution"
|
||||
| "followupAutoApproveTimeoutMs"
|
||||
| "allowedCommands"
|
||||
| "deniedCommands"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
|||
alwaysAllowSubtasks?: boolean
|
||||
alwaysAllowExecute?: boolean
|
||||
alwaysAllowFollowupQuestions?: boolean
|
||||
alwaysAllowDuringTodoExecution?: boolean
|
||||
followupAutoApproveTimeoutMs?: number
|
||||
allowedCommands?: string[]
|
||||
allowedMaxRequests?: number | undefined
|
||||
|
|
@ -46,6 +47,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
|||
| "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)}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ type AutoApproveToggles = Pick<
|
|||
| "alwaysAllowSubtasks"
|
||||
| "alwaysAllowExecute"
|
||||
| "alwaysAllowFollowupQuestions"
|
||||
| "alwaysAllowDuringTodoExecution"
|
||||
>
|
||||
|
||||
export type AutoApproveSetting = keyof AutoApproveToggles
|
||||
|
|
@ -83,6 +84,13 @@ export const autoApproveSettingsConfig: Record<AutoApproveSetting, AutoApproveCo
|
|||
icon: "question",
|
||||
testId: "always-allow-followup-questions-toggle",
|
||||
},
|
||||
alwaysAllowDuringTodoExecution: {
|
||||
key: "alwaysAllowDuringTodoExecution",
|
||||
labelKey: "settings:autoApprove.todoExecution.label",
|
||||
descriptionKey: "settings:autoApprove.todoExecution.description",
|
||||
icon: "checklist",
|
||||
testId: "always-allow-todo-execution-toggle",
|
||||
},
|
||||
}
|
||||
|
||||
type AutoApproveToggleProps = AutoApproveToggles & {
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
customSupportPrompts,
|
||||
profileThresholds,
|
||||
alwaysAllowFollowupQuestions,
|
||||
alwaysAllowDuringTodoExecution,
|
||||
followupAutoApproveTimeoutMs,
|
||||
includeDiagnosticMessages,
|
||||
maxDiagnosticMessages,
|
||||
|
|
@ -404,6 +405,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
alwaysAllowSubtasks={alwaysAllowSubtasks}
|
||||
alwaysAllowExecute={alwaysAllowExecute}
|
||||
alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions}
|
||||
alwaysAllowDuringTodoExecution={alwaysAllowDuringTodoExecution}
|
||||
followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs}
|
||||
allowedCommands={allowedCommands}
|
||||
allowedMaxRequests={allowedMaxRequests ?? undefined}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ describe("AutoApproveToggle", () => {
|
|||
alwaysAllowSubtasks: false,
|
||||
alwaysAllowExecute: true,
|
||||
alwaysAllowFollowupQuestions: false,
|
||||
alwaysAllowDuringTodoExecution: false,
|
||||
onToggle: mockOnToggle,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue