Skip to content

Commit 2dc3b1a

Browse files
committed
feat: add VSCode setting to control todo completion prevention
- Add preventCompletionWithOpenTodos setting (default: false) - Update attemptCompletionTool to check setting before enforcing validation - Add comprehensive tests for setting behavior - Include setting in IPC configuration and global settings schema - Add localization for the new setting
1 parent 790428a commit 2dc3b1a

File tree

5 files changed

+178
-2
lines changed

5 files changed

+178
-2
lines changed

packages/types/src/global-settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const globalSettingsSchema = z.object({
5151
allowedCommands: z.array(z.string()).optional(),
5252
deniedCommands: z.array(z.string()).optional(),
5353
commandExecutionTimeout: z.number().optional(),
54+
preventCompletionWithOpenTodos: z.boolean().optional(),
5455
allowedMaxRequests: z.number().nullish(),
5556
autoCondenseContext: z.boolean().optional(),
5657
autoCondenseContextPercent: z.number().optional(),
@@ -202,6 +203,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
202203
followupAutoApproveTimeoutMs: 0,
203204
allowedCommands: ["*"],
204205
commandExecutionTimeout: 30_000,
206+
preventCompletionWithOpenTodos: false,
205207

206208
browserToolEnabled: false,
207209
browserViewportSize: "900x600",

src/core/tools/__tests__/attemptCompletionTool.spec.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,25 @@ vi.mock("../../prompts/responses", () => ({
99
},
1010
}))
1111

12+
// Mock vscode module
13+
vi.mock("vscode", () => ({
14+
workspace: {
15+
getConfiguration: vi.fn(() => ({
16+
get: vi.fn(),
17+
})),
18+
},
19+
}))
20+
21+
// Mock Package module
22+
vi.mock("../../../shared/package", () => ({
23+
Package: {
24+
name: "roo-cline",
25+
},
26+
}))
27+
1228
import { attemptCompletionTool } from "../attemptCompletionTool"
1329
import { Task } from "../../task/Task"
30+
import * as vscode from "vscode"
1431

1532
describe("attemptCompletionTool", () => {
1633
let mockTask: Partial<Task>
@@ -20,6 +37,7 @@ describe("attemptCompletionTool", () => {
2037
let mockRemoveClosingTag: ReturnType<typeof vi.fn>
2138
let mockToolDescription: ReturnType<typeof vi.fn>
2239
let mockAskFinishSubTaskApproval: ReturnType<typeof vi.fn>
40+
let mockGetConfiguration: ReturnType<typeof vi.fn>
2341

2442
beforeEach(() => {
2543
mockPushToolResult = vi.fn()
@@ -28,6 +46,17 @@ describe("attemptCompletionTool", () => {
2846
mockRemoveClosingTag = vi.fn()
2947
mockToolDescription = vi.fn()
3048
mockAskFinishSubTaskApproval = vi.fn()
49+
mockGetConfiguration = vi.fn(() => ({
50+
get: vi.fn((key: string, defaultValue: any) => {
51+
if (key === "preventCompletionWithOpenTodos") {
52+
return defaultValue // Default to false unless overridden in test
53+
}
54+
return defaultValue
55+
}),
56+
}))
57+
58+
// Setup vscode mock
59+
vi.mocked(vscode.workspace.getConfiguration).mockImplementation(mockGetConfiguration)
3160

3261
mockTask = {
3362
consecutiveMistakeCount: 0,
@@ -224,5 +253,137 @@ describe("attemptCompletionTool", () => {
224253
expect.stringContaining("Cannot complete task while there are incomplete todos"),
225254
)
226255
})
256+
257+
it("should allow completion when setting is disabled even with incomplete todos", async () => {
258+
const block: AttemptCompletionToolUse = {
259+
type: "tool_use",
260+
name: "attempt_completion",
261+
params: { result: "Task completed successfully" },
262+
partial: false,
263+
}
264+
265+
const todosWithPending: TodoItem[] = [
266+
{ id: "1", content: "First task", status: "completed" },
267+
{ id: "2", content: "Second task", status: "pending" },
268+
]
269+
270+
mockTask.todoList = todosWithPending
271+
272+
// Ensure the setting is disabled (default behavior)
273+
mockGetConfiguration.mockReturnValue({
274+
get: vi.fn((key: string, defaultValue: any) => {
275+
if (key === "preventCompletionWithOpenTodos") {
276+
return false // Setting is disabled
277+
}
278+
return defaultValue
279+
}),
280+
})
281+
282+
await attemptCompletionTool(
283+
mockTask as Task,
284+
block,
285+
mockAskApproval,
286+
mockHandleError,
287+
mockPushToolResult,
288+
mockRemoveClosingTag,
289+
mockToolDescription,
290+
mockAskFinishSubTaskApproval,
291+
)
292+
293+
// Should not prevent completion when setting is disabled
294+
expect(mockTask.consecutiveMistakeCount).toBe(0)
295+
expect(mockTask.recordToolError).not.toHaveBeenCalled()
296+
expect(mockPushToolResult).not.toHaveBeenCalledWith(
297+
expect.stringContaining("Cannot complete task while there are incomplete todos"),
298+
)
299+
})
300+
301+
it("should prevent completion when setting is enabled with incomplete todos", async () => {
302+
const block: AttemptCompletionToolUse = {
303+
type: "tool_use",
304+
name: "attempt_completion",
305+
params: { result: "Task completed successfully" },
306+
partial: false,
307+
}
308+
309+
const todosWithPending: TodoItem[] = [
310+
{ id: "1", content: "First task", status: "completed" },
311+
{ id: "2", content: "Second task", status: "pending" },
312+
]
313+
314+
mockTask.todoList = todosWithPending
315+
316+
// Enable the setting
317+
mockGetConfiguration.mockReturnValue({
318+
get: vi.fn((key: string, defaultValue: any) => {
319+
if (key === "preventCompletionWithOpenTodos") {
320+
return true // Setting is enabled
321+
}
322+
return defaultValue
323+
}),
324+
})
325+
326+
await attemptCompletionTool(
327+
mockTask as Task,
328+
block,
329+
mockAskApproval,
330+
mockHandleError,
331+
mockPushToolResult,
332+
mockRemoveClosingTag,
333+
mockToolDescription,
334+
mockAskFinishSubTaskApproval,
335+
)
336+
337+
// Should prevent completion when setting is enabled and there are incomplete todos
338+
expect(mockTask.consecutiveMistakeCount).toBe(1)
339+
expect(mockTask.recordToolError).toHaveBeenCalledWith("attempt_completion")
340+
expect(mockPushToolResult).toHaveBeenCalledWith(
341+
expect.stringContaining("Cannot complete task while there are incomplete todos"),
342+
)
343+
})
344+
345+
it("should allow completion when setting is enabled but all todos are completed", async () => {
346+
const block: AttemptCompletionToolUse = {
347+
type: "tool_use",
348+
name: "attempt_completion",
349+
params: { result: "Task completed successfully" },
350+
partial: false,
351+
}
352+
353+
const completedTodos: TodoItem[] = [
354+
{ id: "1", content: "First task", status: "completed" },
355+
{ id: "2", content: "Second task", status: "completed" },
356+
]
357+
358+
mockTask.todoList = completedTodos
359+
360+
// Enable the setting
361+
mockGetConfiguration.mockReturnValue({
362+
get: vi.fn((key: string, defaultValue: any) => {
363+
if (key === "preventCompletionWithOpenTodos") {
364+
return true // Setting is enabled
365+
}
366+
return defaultValue
367+
}),
368+
})
369+
370+
await attemptCompletionTool(
371+
mockTask as Task,
372+
block,
373+
mockAskApproval,
374+
mockHandleError,
375+
mockPushToolResult,
376+
mockRemoveClosingTag,
377+
mockToolDescription,
378+
mockAskFinishSubTaskApproval,
379+
)
380+
381+
// Should allow completion when setting is enabled but all todos are completed
382+
expect(mockTask.consecutiveMistakeCount).toBe(0)
383+
expect(mockTask.recordToolError).not.toHaveBeenCalled()
384+
expect(mockPushToolResult).not.toHaveBeenCalledWith(
385+
expect.stringContaining("Cannot complete task while there are incomplete todos"),
386+
)
387+
})
227388
})
228389
})

src/core/tools/attemptCompletionTool.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Anthropic from "@anthropic-ai/sdk"
2+
import * as vscode from "vscode"
23

34
import { TelemetryService } from "@roo-code/telemetry"
45

@@ -14,6 +15,7 @@ import {
1415
AskFinishSubTaskApproval,
1516
} from "../../shared/tools"
1617
import { formatResponse } from "../prompts/responses"
18+
import { Package } from "../../shared/package"
1719

1820
export async function attemptCompletionTool(
1921
cline: Task,
@@ -28,10 +30,15 @@ export async function attemptCompletionTool(
2830
const result: string | undefined = block.params.result
2931
const command: string | undefined = block.params.command
3032

31-
// Check if there are incomplete todos
33+
// Get the setting for preventing completion with open todos from VSCode configuration
34+
const preventCompletionWithOpenTodos = vscode.workspace
35+
.getConfiguration(Package.name)
36+
.get<boolean>("preventCompletionWithOpenTodos", false)
37+
38+
// Check if there are incomplete todos (only if the setting is enabled)
3239
const hasIncompleteTodos = cline.todoList && cline.todoList.some((todo) => todo.status !== "completed")
3340

34-
if (hasIncompleteTodos) {
41+
if (preventCompletionWithOpenTodos && hasIncompleteTodos) {
3542
cline.consecutiveMistakeCount++
3643
cline.recordToolError("attempt_completion")
3744
pushToolResult(

src/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,11 @@
345345
"maximum": 600,
346346
"description": "%commands.commandExecutionTimeout.description%"
347347
},
348+
"roo-cline.preventCompletionWithOpenTodos": {
349+
"type": "boolean",
350+
"default": false,
351+
"description": "%commands.preventCompletionWithOpenTodos.description%"
352+
},
348353
"roo-cline.vsCodeLmModelSelector": {
349354
"type": "object",
350355
"properties": {

src/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled",
3030
"commands.deniedCommands.description": "Command prefixes that will be automatically denied without asking for approval. In case of conflicts with allowed commands, the longest prefix match takes precedence. Add * to deny all commands.",
3131
"commands.commandExecutionTimeout.description": "Maximum time in seconds to wait for command execution to complete before timing out (0 = no timeout, 1-600s, default: 0s)",
32+
"commands.preventCompletionWithOpenTodos.description": "Prevent task completion when there are incomplete todos in the todo list (default: false)",
3233
"settings.vsCodeLmModelSelector.description": "Settings for VSCode Language Model API",
3334
"settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)",
3435
"settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)",

0 commit comments

Comments
 (0)