Merge remote-tracking branch 'origin/main' into feature/current-editor-context

This commit is contained in:
Roo Code 2025-07-18 17:30:01 +00:00
commit 01fde9460e
24 changed files with 532 additions and 1 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@roo-code/types",
"version": "1.34.0",
"version": "1.35.0",
"description": "TypeScript type definitions for Roo Code.",
"publishConfig": {
"access": "public",

View file

@ -51,6 +51,7 @@ export const globalSettingsSchema = z.object({
allowedCommands: z.array(z.string()).optional(),
deniedCommands: z.array(z.string()).optional(),
commandExecutionTimeout: z.number().optional(),
preventCompletionWithOpenTodos: z.boolean().optional(),
allowedMaxRequests: z.number().nullish(),
autoCondenseContext: z.boolean().optional(),
autoCondenseContextPercent: z.number().optional(),
@ -202,6 +203,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
followupAutoApproveTimeoutMs: 0,
allowedCommands: ["*"],
commandExecutionTimeout: 30_000,
preventCompletionWithOpenTodos: false,
browserToolEnabled: false,
browserViewportSize: "900x600",

View file

@ -0,0 +1,412 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { TodoItem } from "@roo-code/types"
import { AttemptCompletionToolUse } from "../../../shared/tools"
// Mock the formatResponse module before importing the tool
vi.mock("../../prompts/responses", () => ({
formatResponse: {
toolError: vi.fn((msg: string) => `Error: ${msg}`),
},
}))
// Mock vscode module
vi.mock("vscode", () => ({
workspace: {
getConfiguration: vi.fn(() => ({
get: vi.fn(),
})),
},
}))
// Mock Package module
vi.mock("../../../shared/package", () => ({
Package: {
name: "roo-cline",
},
}))
import { attemptCompletionTool } from "../attemptCompletionTool"
import { Task } from "../../task/Task"
import * as vscode from "vscode"
describe("attemptCompletionTool", () => {
let mockTask: Partial<Task>
let mockPushToolResult: ReturnType<typeof vi.fn>
let mockAskApproval: ReturnType<typeof vi.fn>
let mockHandleError: ReturnType<typeof vi.fn>
let mockRemoveClosingTag: ReturnType<typeof vi.fn>
let mockToolDescription: ReturnType<typeof vi.fn>
let mockAskFinishSubTaskApproval: ReturnType<typeof vi.fn>
let mockGetConfiguration: ReturnType<typeof vi.fn>
beforeEach(() => {
mockPushToolResult = vi.fn()
mockAskApproval = vi.fn()
mockHandleError = vi.fn()
mockRemoveClosingTag = vi.fn()
mockToolDescription = vi.fn()
mockAskFinishSubTaskApproval = vi.fn()
mockGetConfiguration = vi.fn(() => ({
get: vi.fn((key: string, defaultValue: any) => {
if (key === "preventCompletionWithOpenTodos") {
return defaultValue // Default to false unless overridden in test
}
return defaultValue
}),
}))
// Setup vscode mock
vi.mocked(vscode.workspace.getConfiguration).mockImplementation(mockGetConfiguration)
mockTask = {
consecutiveMistakeCount: 0,
recordToolError: vi.fn(),
todoList: undefined,
}
})
describe("todo list validation", () => {
it("should allow completion when there is no todo list", async () => {
const block: AttemptCompletionToolUse = {
type: "tool_use",
name: "attempt_completion",
params: { result: "Task completed successfully" },
partial: false,
}
mockTask.todoList = undefined
await attemptCompletionTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
mockToolDescription,
mockAskFinishSubTaskApproval,
)
// Should not call pushToolResult with an error for empty todo list
expect(mockTask.consecutiveMistakeCount).toBe(0)
expect(mockTask.recordToolError).not.toHaveBeenCalled()
})
it("should allow completion when todo list is empty", async () => {
const block: AttemptCompletionToolUse = {
type: "tool_use",
name: "attempt_completion",
params: { result: "Task completed successfully" },
partial: false,
}
mockTask.todoList = []
await attemptCompletionTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
mockToolDescription,
mockAskFinishSubTaskApproval,
)
expect(mockTask.consecutiveMistakeCount).toBe(0)
expect(mockTask.recordToolError).not.toHaveBeenCalled()
})
it("should allow completion when all todos are completed", async () => {
const block: AttemptCompletionToolUse = {
type: "tool_use",
name: "attempt_completion",
params: { result: "Task completed successfully" },
partial: false,
}
const completedTodos: TodoItem[] = [
{ id: "1", content: "First task", status: "completed" },
{ id: "2", content: "Second task", status: "completed" },
]
mockTask.todoList = completedTodos
await attemptCompletionTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
mockToolDescription,
mockAskFinishSubTaskApproval,
)
expect(mockTask.consecutiveMistakeCount).toBe(0)
expect(mockTask.recordToolError).not.toHaveBeenCalled()
})
it("should prevent completion when there are pending todos", async () => {
const block: AttemptCompletionToolUse = {
type: "tool_use",
name: "attempt_completion",
params: { result: "Task completed successfully" },
partial: false,
}
const todosWithPending: TodoItem[] = [
{ id: "1", content: "First task", status: "completed" },
{ id: "2", content: "Second task", status: "pending" },
]
mockTask.todoList = todosWithPending
// Enable the setting to prevent completion with open todos
mockGetConfiguration.mockReturnValue({
get: vi.fn((key: string, defaultValue: any) => {
if (key === "preventCompletionWithOpenTodos") {
return true // Setting is enabled
}
return defaultValue
}),
})
await attemptCompletionTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
mockToolDescription,
mockAskFinishSubTaskApproval,
)
expect(mockTask.consecutiveMistakeCount).toBe(1)
expect(mockTask.recordToolError).toHaveBeenCalledWith("attempt_completion")
expect(mockPushToolResult).toHaveBeenCalledWith(
expect.stringContaining("Cannot complete task while there are incomplete todos"),
)
})
it("should prevent completion when there are in-progress todos", async () => {
const block: AttemptCompletionToolUse = {
type: "tool_use",
name: "attempt_completion",
params: { result: "Task completed successfully" },
partial: false,
}
const todosWithInProgress: TodoItem[] = [
{ id: "1", content: "First task", status: "completed" },
{ id: "2", content: "Second task", status: "in_progress" },
]
mockTask.todoList = todosWithInProgress
// Enable the setting to prevent completion with open todos
mockGetConfiguration.mockReturnValue({
get: vi.fn((key: string, defaultValue: any) => {
if (key === "preventCompletionWithOpenTodos") {
return true // Setting is enabled
}
return defaultValue
}),
})
await attemptCompletionTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
mockToolDescription,
mockAskFinishSubTaskApproval,
)
expect(mockTask.consecutiveMistakeCount).toBe(1)
expect(mockTask.recordToolError).toHaveBeenCalledWith("attempt_completion")
expect(mockPushToolResult).toHaveBeenCalledWith(
expect.stringContaining("Cannot complete task while there are incomplete todos"),
)
})
it("should prevent completion when there are mixed incomplete todos", async () => {
const block: AttemptCompletionToolUse = {
type: "tool_use",
name: "attempt_completion",
params: { result: "Task completed successfully" },
partial: false,
}
const mixedTodos: TodoItem[] = [
{ id: "1", content: "First task", status: "completed" },
{ id: "2", content: "Second task", status: "pending" },
{ id: "3", content: "Third task", status: "in_progress" },
]
mockTask.todoList = mixedTodos
// Enable the setting to prevent completion with open todos
mockGetConfiguration.mockReturnValue({
get: vi.fn((key: string, defaultValue: any) => {
if (key === "preventCompletionWithOpenTodos") {
return true // Setting is enabled
}
return defaultValue
}),
})
await attemptCompletionTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
mockToolDescription,
mockAskFinishSubTaskApproval,
)
expect(mockTask.consecutiveMistakeCount).toBe(1)
expect(mockTask.recordToolError).toHaveBeenCalledWith("attempt_completion")
expect(mockPushToolResult).toHaveBeenCalledWith(
expect.stringContaining("Cannot complete task while there are incomplete todos"),
)
})
it("should allow completion when setting is disabled even with incomplete todos", async () => {
const block: AttemptCompletionToolUse = {
type: "tool_use",
name: "attempt_completion",
params: { result: "Task completed successfully" },
partial: false,
}
const todosWithPending: TodoItem[] = [
{ id: "1", content: "First task", status: "completed" },
{ id: "2", content: "Second task", status: "pending" },
]
mockTask.todoList = todosWithPending
// Ensure the setting is disabled (default behavior)
mockGetConfiguration.mockReturnValue({
get: vi.fn((key: string, defaultValue: any) => {
if (key === "preventCompletionWithOpenTodos") {
return false // Setting is disabled
}
return defaultValue
}),
})
await attemptCompletionTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
mockToolDescription,
mockAskFinishSubTaskApproval,
)
// Should not prevent completion when setting is disabled
expect(mockTask.consecutiveMistakeCount).toBe(0)
expect(mockTask.recordToolError).not.toHaveBeenCalled()
expect(mockPushToolResult).not.toHaveBeenCalledWith(
expect.stringContaining("Cannot complete task while there are incomplete todos"),
)
})
it("should prevent completion when setting is enabled with incomplete todos", async () => {
const block: AttemptCompletionToolUse = {
type: "tool_use",
name: "attempt_completion",
params: { result: "Task completed successfully" },
partial: false,
}
const todosWithPending: TodoItem[] = [
{ id: "1", content: "First task", status: "completed" },
{ id: "2", content: "Second task", status: "pending" },
]
mockTask.todoList = todosWithPending
// Enable the setting
mockGetConfiguration.mockReturnValue({
get: vi.fn((key: string, defaultValue: any) => {
if (key === "preventCompletionWithOpenTodos") {
return true // Setting is enabled
}
return defaultValue
}),
})
await attemptCompletionTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
mockToolDescription,
mockAskFinishSubTaskApproval,
)
// Should prevent completion when setting is enabled and there are incomplete todos
expect(mockTask.consecutiveMistakeCount).toBe(1)
expect(mockTask.recordToolError).toHaveBeenCalledWith("attempt_completion")
expect(mockPushToolResult).toHaveBeenCalledWith(
expect.stringContaining("Cannot complete task while there are incomplete todos"),
)
})
it("should allow completion when setting is enabled but all todos are completed", async () => {
const block: AttemptCompletionToolUse = {
type: "tool_use",
name: "attempt_completion",
params: { result: "Task completed successfully" },
partial: false,
}
const completedTodos: TodoItem[] = [
{ id: "1", content: "First task", status: "completed" },
{ id: "2", content: "Second task", status: "completed" },
]
mockTask.todoList = completedTodos
// Enable the setting
mockGetConfiguration.mockReturnValue({
get: vi.fn((key: string, defaultValue: any) => {
if (key === "preventCompletionWithOpenTodos") {
return true // Setting is enabled
}
return defaultValue
}),
})
await attemptCompletionTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
mockToolDescription,
mockAskFinishSubTaskApproval,
)
// Should allow completion when setting is enabled but all todos are completed
expect(mockTask.consecutiveMistakeCount).toBe(0)
expect(mockTask.recordToolError).not.toHaveBeenCalled()
expect(mockPushToolResult).not.toHaveBeenCalledWith(
expect.stringContaining("Cannot complete task while there are incomplete todos"),
)
})
})
})

View file

@ -1,4 +1,5 @@
import Anthropic from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import { TelemetryService } from "@roo-code/telemetry"
@ -14,6 +15,7 @@ import {
AskFinishSubTaskApproval,
} from "../../shared/tools"
import { formatResponse } from "../prompts/responses"
import { Package } from "../../shared/package"
export async function attemptCompletionTool(
cline: Task,
@ -28,6 +30,25 @@ export async function attemptCompletionTool(
const result: string | undefined = block.params.result
const command: string | undefined = block.params.command
// Get the setting for preventing completion with open todos from VSCode configuration
const preventCompletionWithOpenTodos = vscode.workspace
.getConfiguration(Package.name)
.get<boolean>("preventCompletionWithOpenTodos", false)
// Check if there are incomplete todos (only if the setting is enabled)
const hasIncompleteTodos = cline.todoList && cline.todoList.some((todo) => todo.status !== "completed")
if (preventCompletionWithOpenTodos && hasIncompleteTodos) {
cline.consecutiveMistakeCount++
cline.recordToolError("attempt_completion")
pushToolResult(
formatResponse.toolError(
"Cannot complete task while there are incomplete todos. Please finish all todos before attempting completion.",
),
)
return
}
try {
const lastMessage = cline.clineMessages.at(-1)

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "Esteu segur que voleu suprimir aquest mode personalitzat?",
"confirm": "Suprimeix"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Evitar la finalització de tasques quan hi ha todos incomplets a la llista de todos"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "Bist du sicher, dass du diesen benutzerdefinierten Modus löschen möchtest?",
"confirm": "Löschen"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Aufgabenabschluss verhindern, wenn unvollständige Todos in der Todo-Liste vorhanden sind"
}
}
}

View file

@ -155,5 +155,10 @@
"descriptionNoRules": "Are you sure you want to delete this custom mode?",
"confirm": "Delete"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Prevent task completion when there are incomplete todos in the todo list"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "¿Estás seguro de que quieres eliminar este modo personalizado?",
"confirm": "Eliminar"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Prevenir la finalización de tareas cuando hay todos incompletos en la lista de todos"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "Êtes-vous sûr de vouloir supprimer ce mode personnalisé ?",
"confirm": "Supprimer"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Empêcher la finalisation des tâches lorsqu'il y a des todos incomplets dans la liste de todos"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "क्या आप वाकई इस कस्टम मोड को हटाना चाहते हैं?",
"confirm": "हटाएं"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "जब टूडू सूची में अधूरे टूडू हों तो कार्य पूर्ण होने से रोकें"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "Anda yakin ingin menghapus mode kustom ini?",
"confirm": "Hapus"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Mencegah penyelesaian tugas ketika ada todo yang belum selesai dalam daftar todo"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "Sei sicuro di voler eliminare questa modalità personalizzata?",
"confirm": "Elimina"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Impedire il completamento delle attività quando ci sono todo incompleti nella lista dei todo"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "このカスタムモードを削除してもよろしいですか?",
"confirm": "削除"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Todoリストに未完了のTodoがある場合、タスクの完了を防ぐ"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "이 사용자 정의 모드를 삭제하시겠습니까?",
"confirm": "삭제"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "할 일 목록에 미완료된 할 일이 있을 때 작업 완료를 방지"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "Weet je zeker dat je deze aangepaste modus wilt verwijderen?",
"confirm": "Verwijderen"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Voorkom taakafronding wanneer er onvolledige todos in de todolijst staan"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "Czy na pewno chcesz usunąć ten tryb niestandardowy?",
"confirm": "Usuń"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Zapobiegaj ukończeniu zadania gdy na liście zadań są nieukończone zadania"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "Tem certeza de que deseja excluir este modo personalizado?",
"confirm": "Excluir"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Impedir a conclusão de tarefas quando há todos incompletos na lista de todos"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "Вы уверены, что хотите удалить этот пользовательский режим?",
"confirm": "Удалить"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Предотвратить завершение задач при наличии незавершенных дел в списке дел"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "Bu özel modu silmek istediğinizden emin misiniz?",
"confirm": "Sil"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Todo listesinde tamamlanmamış todolar olduğunda görev tamamlanmasını engelle"
}
}
}

View file

@ -173,5 +173,10 @@
"descriptionNoRules": "Bạn có chắc chắn muốn xóa chế độ tùy chỉnh này không?",
"confirm": "Xóa"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "Ngăn chặn hoàn thành nhiệm vụ khi có các todo chưa hoàn thành trong danh sách todo"
}
}
}

View file

@ -171,5 +171,10 @@
"descriptionNoRules": "您确定要删除此自定义模式吗?",
"confirm": "删除"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "当待办事项列表中有未完成的待办事项时阻止任务完成"
}
}
}

View file

@ -166,5 +166,10 @@
"descriptionNoRules": "您確定要刪除此自訂模式嗎?",
"confirm": "刪除"
}
},
"commands": {
"preventCompletionWithOpenTodos": {
"description": "當待辦事項清單中有未完成的待辦事項時阻止工作完成"
}
}
}

View file

@ -345,6 +345,11 @@
"maximum": 600,
"description": "%commands.commandExecutionTimeout.description%"
},
"roo-cline.preventCompletionWithOpenTodos": {
"type": "boolean",
"default": false,
"description": "%commands.preventCompletionWithOpenTodos.description%"
},
"roo-cline.vsCodeLmModelSelector": {
"type": "object",
"properties": {

View file

@ -29,6 +29,7 @@
"commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled",
"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.",
"commands.commandExecutionTimeout.description": "Maximum time in seconds to wait for command execution to complete before timing out (0 = no timeout, 1-600s, default: 0s)",
"commands.preventCompletionWithOpenTodos.description": "Prevent task completion when there are incomplete todos in the todo list",
"settings.vsCodeLmModelSelector.description": "Settings for VSCode Language Model API",
"settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)",
"settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)",