mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add optional todos parameter to new_task tool
- Added todos parameter to NewTaskToolUse interface - Updated new_task tool description with todos parameter documentation - Modified newTaskTool.ts to extract and pass todos to initClineWithTask - Updated ClineProvider.initClineWithTask to accept todos in options - Enhanced Task constructor to parse initial todos from markdown format - Added comprehensive tests for todo parsing functionality - Fixed bug where empty string todos were not handled correctly
This commit is contained in:
parent
7a6e852248
commit
52a8aa6e10
7 changed files with 357 additions and 4 deletions
|
|
@ -7,11 +7,18 @@ Description: This will let you create a new task instance in the chosen mode usi
|
|||
Parameters:
|
||||
- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
|
||||
- message: (required) The initial user message or instructions for this new task.
|
||||
- todos: (optional) A markdown checklist of todo items to initialize the new task with. Use the same format as update_todo_list.
|
||||
|
||||
Usage:
|
||||
<new_task>
|
||||
<mode>your-mode-slug-here</mode>
|
||||
<message>Your initial instructions here</message>
|
||||
<todos>
|
||||
[ ] First todo item
|
||||
[ ] Second todo item
|
||||
[x] Completed todo item
|
||||
[-] In progress todo item
|
||||
</todos>
|
||||
</new_task>
|
||||
|
||||
Example:
|
||||
|
|
@ -19,5 +26,18 @@ Example:
|
|||
<mode>code</mode>
|
||||
<message>Implement a new feature for the application.</message>
|
||||
</new_task>
|
||||
|
||||
Example with todos:
|
||||
<new_task>
|
||||
<mode>code</mode>
|
||||
<message>Implement user authentication</message>
|
||||
<todos>
|
||||
[ ] Set up authentication middleware
|
||||
[ ] Create login endpoint
|
||||
[ ] Create logout endpoint
|
||||
[ ] Add session management
|
||||
[ ] Write tests for authentication
|
||||
</todos>
|
||||
</new_task>
|
||||
`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ export type TaskOptions = {
|
|||
parentTask?: Task
|
||||
taskNumber?: number
|
||||
onCreated?: (cline: Task) => void
|
||||
todos?: string
|
||||
}
|
||||
|
||||
export class Task extends EventEmitter<ClineEvents> {
|
||||
|
|
@ -228,6 +229,7 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
parentTask,
|
||||
taskNumber = -1,
|
||||
onCreated,
|
||||
todos,
|
||||
}: TaskOptions) {
|
||||
super()
|
||||
|
||||
|
|
@ -294,6 +296,11 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
|
||||
this.toolRepetitionDetector = new ToolRepetitionDetector(this.consecutiveMistakeLimit)
|
||||
|
||||
// Parse and initialize todos if provided
|
||||
if (todos !== undefined) {
|
||||
this.parseTodosFromMarkdown(todos)
|
||||
}
|
||||
|
||||
onCreated?.(this)
|
||||
|
||||
if (startTask) {
|
||||
|
|
@ -1956,4 +1963,42 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
public get cwd() {
|
||||
return this.workspacePath
|
||||
}
|
||||
|
||||
// Todo parsing
|
||||
private parseTodosFromMarkdown(todosMarkdown: string): void {
|
||||
if (typeof todosMarkdown !== "string") {
|
||||
return
|
||||
}
|
||||
|
||||
this.todoList = []
|
||||
|
||||
if (!todosMarkdown) {
|
||||
return
|
||||
}
|
||||
|
||||
const lines = todosMarkdown
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^\[\s*([ xX\-~])\s*\]\s+(.+)$/)
|
||||
if (!match) continue
|
||||
|
||||
let status: "pending" | "in_progress" | "completed" = "pending"
|
||||
if (match[1] === "x" || match[1] === "X") status = "completed"
|
||||
else if (match[1] === "-" || match[1] === "~") status = "in_progress"
|
||||
|
||||
const id = crypto
|
||||
.createHash("md5")
|
||||
.update(match[2] + status)
|
||||
.digest("hex")
|
||||
|
||||
this.todoList.push({
|
||||
id,
|
||||
content: match[2],
|
||||
status,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -389,6 +389,157 @@ describe("Cline", () => {
|
|||
new Task({ provider: mockProvider, apiConfiguration: mockApiConfig })
|
||||
}).toThrow("Either historyItem or task/images must be provided")
|
||||
})
|
||||
|
||||
it("should parse todos from markdown checklist when provided", () => {
|
||||
const todosMarkdown = `[x] Design the architecture
|
||||
[-] Write the code
|
||||
[ ] Add tests
|
||||
[ ] Update documentation`
|
||||
|
||||
const task = new Task({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
task: "test task",
|
||||
todos: todosMarkdown,
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
expect(task.todoList).toBeDefined()
|
||||
expect(task.todoList).toHaveLength(4)
|
||||
|
||||
expect(task.todoList![0].content).toBe("Design the architecture")
|
||||
expect(task.todoList![0].status).toBe("completed")
|
||||
|
||||
expect(task.todoList![1].content).toBe("Write the code")
|
||||
expect(task.todoList![1].status).toBe("in_progress")
|
||||
|
||||
expect(task.todoList![2].content).toBe("Add tests")
|
||||
expect(task.todoList![2].status).toBe("pending")
|
||||
|
||||
expect(task.todoList![3].content).toBe("Update documentation")
|
||||
expect(task.todoList![3].status).toBe("pending")
|
||||
})
|
||||
|
||||
it("should handle various markdown checklist formats", () => {
|
||||
const todosMarkdown = `[X] Uppercase completed
|
||||
[~] Alternative in-progress
|
||||
[ ] Extra spaces
|
||||
[x]No space after bracket`
|
||||
|
||||
const task = new Task({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
task: "test task",
|
||||
todos: todosMarkdown,
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
expect(task.todoList).toBeDefined()
|
||||
expect(task.todoList).toHaveLength(3) // "No space after bracket" won't match the regex
|
||||
|
||||
expect(task.todoList![0].content).toBe("Uppercase completed")
|
||||
expect(task.todoList![0].status).toBe("completed")
|
||||
|
||||
expect(task.todoList![1].content).toBe("Alternative in-progress")
|
||||
expect(task.todoList![1].status).toBe("in_progress")
|
||||
|
||||
expect(task.todoList![2].content).toBe("Extra spaces")
|
||||
expect(task.todoList![2].status).toBe("pending")
|
||||
})
|
||||
|
||||
it("should handle empty todos parameter", () => {
|
||||
const task = new Task({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
task: "test task",
|
||||
todos: "",
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
expect(task.todoList).toBeDefined()
|
||||
expect(task.todoList).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle undefined todos parameter", () => {
|
||||
const task = new Task({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
task: "test task",
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
expect(task.todoList).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle todos with special characters", () => {
|
||||
const todosMarkdown = `[x] Handle @mentions and $variables
|
||||
[ ] Support | pipes and \\ backslashes
|
||||
[-] Test "quotes" and 'apostrophes'`
|
||||
|
||||
const task = new Task({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
task: "test task",
|
||||
todos: todosMarkdown,
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
expect(task.todoList).toBeDefined()
|
||||
expect(task.todoList).toHaveLength(3)
|
||||
|
||||
expect(task.todoList![0].content).toBe("Handle @mentions and $variables")
|
||||
expect(task.todoList![1].content).toBe("Support | pipes and \\ backslashes")
|
||||
expect(task.todoList![2].content).toBe("Test \"quotes\" and 'apostrophes'")
|
||||
})
|
||||
|
||||
it("should generate unique IDs for todos", () => {
|
||||
const todosMarkdown = `[ ] First task
|
||||
[ ] Second task
|
||||
[ ] Third task`
|
||||
|
||||
const task = new Task({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
task: "test task",
|
||||
todos: todosMarkdown,
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
expect(task.todoList).toBeDefined()
|
||||
expect(task.todoList).toHaveLength(3)
|
||||
|
||||
// Check that all IDs are unique
|
||||
const ids = task.todoList!.map((todo) => todo.id)
|
||||
const uniqueIds = new Set(ids)
|
||||
expect(uniqueIds.size).toBe(3)
|
||||
|
||||
// Check that IDs are valid MD5 hashes (32 hex characters)
|
||||
ids.forEach((id) => {
|
||||
expect(id).toMatch(/^[a-f0-9]{32}$/)
|
||||
})
|
||||
})
|
||||
|
||||
it("should ignore invalid checklist lines", () => {
|
||||
const todosMarkdown = `[x] Valid task
|
||||
This is not a checklist item
|
||||
- [ ] This format is not supported
|
||||
[x] Another valid task
|
||||
Random text`
|
||||
|
||||
const task = new Task({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
task: "test task",
|
||||
todos: todosMarkdown,
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
expect(task.todoList).toBeDefined()
|
||||
expect(task.todoList).toHaveLength(2)
|
||||
|
||||
expect(task.todoList![0].content).toBe("Valid task")
|
||||
expect(task.todoList![1].content).toBe("Another valid task")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getEnvironmentDetails", () => {
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ describe("newTaskTool", () => {
|
|||
"Review this: \\@file1.txt and also \\\\\\@file2.txt", // Unit Test Expectation: \\@ -> \@, \\\\@ -> \\\\@
|
||||
undefined,
|
||||
mockCline,
|
||||
{ todos: undefined },
|
||||
)
|
||||
|
||||
// Verify side effects
|
||||
|
|
@ -126,6 +127,7 @@ describe("newTaskTool", () => {
|
|||
"This is already unescaped: \\@file1.txt", // Expected: \@ remains \@
|
||||
undefined,
|
||||
mockCline,
|
||||
{ todos: undefined },
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -153,6 +155,7 @@ describe("newTaskTool", () => {
|
|||
"A normal mention @file1.txt", // Expected: @ remains @
|
||||
undefined,
|
||||
mockCline,
|
||||
{ todos: undefined },
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -180,8 +183,136 @@ describe("newTaskTool", () => {
|
|||
"Mix: @file0.txt, \\@file1.txt, \\@file2.txt, \\\\\\@file3.txt", // Unit Test Expectation: @->@, \@->\@, \\@->\@, \\\\@->\\\\@
|
||||
undefined,
|
||||
mockCline,
|
||||
{ todos: undefined },
|
||||
)
|
||||
})
|
||||
|
||||
it("should pass todos parameter to initClineWithTask when provided", async () => {
|
||||
const block: ToolUse = {
|
||||
type: "tool_use",
|
||||
name: "new_task",
|
||||
params: {
|
||||
mode: "code",
|
||||
message: "Implement a new feature",
|
||||
todos: "[x] Design the architecture\n[-] Write the code\n[ ] Add tests\n[ ] Update documentation",
|
||||
},
|
||||
partial: false,
|
||||
}
|
||||
|
||||
await newTaskTool(
|
||||
mockCline as any,
|
||||
block,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockInitClineWithTask).toHaveBeenCalledWith("Implement a new feature", undefined, mockCline, {
|
||||
todos: "[x] Design the architecture\n[-] Write the code\n[ ] Add tests\n[ ] Update documentation",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle todos parameter with various markdown checklist formats", async () => {
|
||||
const block: ToolUse = {
|
||||
type: "tool_use",
|
||||
name: "new_task",
|
||||
params: {
|
||||
mode: "code",
|
||||
message: "Complex task",
|
||||
todos: "[X] Completed uppercase\n[~] Alternative in-progress\n[ ] Pending with extra spaces\n[x]No space after bracket",
|
||||
},
|
||||
partial: false,
|
||||
}
|
||||
|
||||
await newTaskTool(
|
||||
mockCline as any,
|
||||
block,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockInitClineWithTask).toHaveBeenCalledWith("Complex task", undefined, mockCline, {
|
||||
todos: "[X] Completed uppercase\n[~] Alternative in-progress\n[ ] Pending with extra spaces\n[x]No space after bracket",
|
||||
})
|
||||
})
|
||||
|
||||
it("should work without todos parameter (backward compatibility)", async () => {
|
||||
const block: ToolUse = {
|
||||
type: "tool_use",
|
||||
name: "new_task",
|
||||
params: {
|
||||
mode: "code",
|
||||
message: "Task without todos",
|
||||
},
|
||||
partial: false,
|
||||
}
|
||||
|
||||
await newTaskTool(
|
||||
mockCline as any,
|
||||
block,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
// Should be called without the todos in options
|
||||
expect(mockInitClineWithTask).toHaveBeenCalledWith("Task without todos", undefined, mockCline, {})
|
||||
})
|
||||
|
||||
it("should handle empty todos parameter", async () => {
|
||||
const block: ToolUse = {
|
||||
type: "tool_use",
|
||||
name: "new_task",
|
||||
params: {
|
||||
mode: "code",
|
||||
message: "Task with empty todos",
|
||||
todos: "",
|
||||
},
|
||||
partial: false,
|
||||
}
|
||||
|
||||
await newTaskTool(
|
||||
mockCline as any,
|
||||
block,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
// Empty string should still be passed
|
||||
expect(mockInitClineWithTask).toHaveBeenCalledWith("Task with empty todos", undefined, mockCline, { todos: "" })
|
||||
})
|
||||
|
||||
it("should handle todos with special characters and escaping", async () => {
|
||||
const block: ToolUse = {
|
||||
type: "tool_use",
|
||||
name: "new_task",
|
||||
params: {
|
||||
mode: "code",
|
||||
message: "Task with special todos",
|
||||
todos: "[x] Handle \\@mentions in todos\n[ ] Support | pipes and \\\\ backslashes\n[-] Test \"quotes\" and 'apostrophes'",
|
||||
},
|
||||
partial: false,
|
||||
}
|
||||
|
||||
await newTaskTool(
|
||||
mockCline as any,
|
||||
block,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockInitClineWithTask).toHaveBeenCalledWith("Task with special todos", undefined, mockCline, {
|
||||
todos: "[x] Handle \\@mentions in todos\n[ ] Support | pipes and \\\\ backslashes\n[-] Test \"quotes\" and 'apostrophes'",
|
||||
})
|
||||
})
|
||||
|
||||
// Add more tests for error handling (missing params, invalid mode, approval denied) if needed
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export async function newTaskTool(
|
|||
) {
|
||||
const mode: string | undefined = block.params.mode
|
||||
const message: string | undefined = block.params.message
|
||||
const todos: string | undefined = block.params.todos
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
|
|
@ -23,6 +24,7 @@ export async function newTaskTool(
|
|||
tool: "newTask",
|
||||
mode: removeClosingTag("mode", mode),
|
||||
content: removeClosingTag("message", message),
|
||||
todos: removeClosingTag("todos", todos),
|
||||
})
|
||||
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
|
|
@ -59,6 +61,7 @@ export async function newTaskTool(
|
|||
tool: "newTask",
|
||||
mode: targetMode.name,
|
||||
content: message,
|
||||
todos: todos,
|
||||
})
|
||||
|
||||
const didApprove = await askApproval("tool", toolMessage)
|
||||
|
|
@ -86,7 +89,7 @@ export async function newTaskTool(
|
|||
// Delay to allow mode change to take effect before next tool is executed.
|
||||
await delay(500)
|
||||
|
||||
const newCline = await provider.initClineWithTask(unescapedMessage, undefined, cline)
|
||||
const newCline = await provider.initClineWithTask(unescapedMessage, undefined, cline, { todos })
|
||||
if (!newCline) {
|
||||
pushToolResult(t("tools:newTask.errors.policy_restriction"))
|
||||
return
|
||||
|
|
|
|||
|
|
@ -534,7 +534,7 @@ export class ClineProvider
|
|||
TaskOptions,
|
||||
"enableDiff" | "enableCheckpoints" | "fuzzyMatchThreshold" | "consecutiveMistakeLimit" | "experiments"
|
||||
>
|
||||
> = {},
|
||||
> & { todos?: string } = {},
|
||||
) {
|
||||
const {
|
||||
apiConfiguration,
|
||||
|
|
@ -549,6 +549,8 @@ export class ClineProvider
|
|||
throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist"))
|
||||
}
|
||||
|
||||
const { todos, ...taskOptions } = options
|
||||
|
||||
const cline = new Task({
|
||||
provider: this,
|
||||
apiConfiguration,
|
||||
|
|
@ -563,7 +565,8 @@ export class ClineProvider
|
|||
parentTask,
|
||||
taskNumber: this.clineStack.length + 1,
|
||||
onCreated: (cline) => this.emit("clineCreated", cline),
|
||||
...options,
|
||||
todos,
|
||||
...taskOptions,
|
||||
})
|
||||
|
||||
await this.addClineToStack(cline)
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export interface SwitchModeToolUse extends ToolUse {
|
|||
|
||||
export interface NewTaskToolUse extends ToolUse {
|
||||
name: "new_task"
|
||||
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message">>
|
||||
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message" | "todos">>
|
||||
}
|
||||
|
||||
export interface SearchAndReplaceToolUse extends ToolUse {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue