diff --git a/src/core/prompts/tools/new-task.ts b/src/core/prompts/tools/new-task.ts
index 7301b7b422..a8cfd9c5d2 100644
--- a/src/core/prompts/tools/new-task.ts
+++ b/src/core/prompts/tools/new-task.ts
@@ -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:
your-mode-slug-here
Your initial instructions here
+
+[ ] First todo item
+[ ] Second todo item
+[x] Completed todo item
+[-] In progress todo item
+
Example:
@@ -19,5 +26,18 @@ Example:
code
Implement a new feature for the application.
+
+Example with todos:
+
+code
+Implement user authentication
+
+[ ] Set up authentication middleware
+[ ] Create login endpoint
+[ ] Create logout endpoint
+[ ] Add session management
+[ ] Write tests for authentication
+
+
`
}
diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts
index fe8fd0f68f..c7e43db184 100644
--- a/src/core/task/Task.ts
+++ b/src/core/task/Task.ts
@@ -126,6 +126,7 @@ export type TaskOptions = {
parentTask?: Task
taskNumber?: number
onCreated?: (cline: Task) => void
+ todos?: string
}
export class Task extends EventEmitter {
@@ -228,6 +229,7 @@ export class Task extends EventEmitter {
parentTask,
taskNumber = -1,
onCreated,
+ todos,
}: TaskOptions) {
super()
@@ -294,6 +296,11 @@ export class Task extends EventEmitter {
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 {
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,
+ })
+ }
+ }
}
diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts
index 9aa5a8d7a8..e941860a10 100644
--- a/src/core/task/__tests__/Task.spec.ts
+++ b/src/core/task/__tests__/Task.spec.ts
@@ -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", () => {
diff --git a/src/core/tools/__tests__/newTaskTool.spec.ts b/src/core/tools/__tests__/newTaskTool.spec.ts
index 1dd79d6e98..a663139aa6 100644
--- a/src/core/tools/__tests__/newTaskTool.spec.ts
+++ b/src/core/tools/__tests__/newTaskTool.spec.ts
@@ -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
})
diff --git a/src/core/tools/newTaskTool.ts b/src/core/tools/newTaskTool.ts
index 7cc7063b49..d54aa5428c 100644
--- a/src/core/tools/newTaskTool.ts
+++ b/src/core/tools/newTaskTool.ts
@@ -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
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 6bcb85e337..4154435bd1 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -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)
diff --git a/src/shared/tools.ts b/src/shared/tools.ts
index 67972243fe..047c2fe351 100644
--- a/src/shared/tools.ts
+++ b/src/shared/tools.ts
@@ -155,7 +155,7 @@ export interface SwitchModeToolUse extends ToolUse {
export interface NewTaskToolUse extends ToolUse {
name: "new_task"
- params: Partial, "mode" | "message">>
+ params: Partial, "mode" | "message" | "todos">>
}
export interface SearchAndReplaceToolUse extends ToolUse {