Skip to content

Commit f7a6589

Browse files
fix: support dash prefix in parseMarkdownChecklist for todo lists (RooCodeInc#8055)
- Updated regex pattern to support optional dash prefix (e.g., "- [ ] Task") - Added comprehensive test coverage for both formats - Fixes issue where todo lists with dash prefixes were not being parsed correctly Fixes RooCodeInc#8054 Co-authored-by: Roo Code <[email protected]>
1 parent b75ef1d commit f7a6589

File tree

2 files changed

+245
-1
lines changed

2 files changed

+245
-1
lines changed
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { describe, it, expect, beforeEach, vi } from "vitest"
2+
import { parseMarkdownChecklist } from "../updateTodoListTool"
3+
import { TodoItem } from "@roo-code/types"
4+
5+
describe("parseMarkdownChecklist", () => {
6+
describe("standard checkbox format (without dash prefix)", () => {
7+
it("should parse pending tasks", () => {
8+
const md = `[ ] Task 1
9+
[ ] Task 2`
10+
const result = parseMarkdownChecklist(md)
11+
expect(result).toHaveLength(2)
12+
expect(result[0].content).toBe("Task 1")
13+
expect(result[0].status).toBe("pending")
14+
expect(result[1].content).toBe("Task 2")
15+
expect(result[1].status).toBe("pending")
16+
})
17+
18+
it("should parse completed tasks with lowercase x", () => {
19+
const md = `[x] Completed task 1
20+
[x] Completed task 2`
21+
const result = parseMarkdownChecklist(md)
22+
expect(result).toHaveLength(2)
23+
expect(result[0].content).toBe("Completed task 1")
24+
expect(result[0].status).toBe("completed")
25+
expect(result[1].content).toBe("Completed task 2")
26+
expect(result[1].status).toBe("completed")
27+
})
28+
29+
it("should parse completed tasks with uppercase X", () => {
30+
const md = `[X] Completed task 1
31+
[X] Completed task 2`
32+
const result = parseMarkdownChecklist(md)
33+
expect(result).toHaveLength(2)
34+
expect(result[0].content).toBe("Completed task 1")
35+
expect(result[0].status).toBe("completed")
36+
expect(result[1].content).toBe("Completed task 2")
37+
expect(result[1].status).toBe("completed")
38+
})
39+
40+
it("should parse in-progress tasks with dash", () => {
41+
const md = `[-] In progress task 1
42+
[-] In progress task 2`
43+
const result = parseMarkdownChecklist(md)
44+
expect(result).toHaveLength(2)
45+
expect(result[0].content).toBe("In progress task 1")
46+
expect(result[0].status).toBe("in_progress")
47+
expect(result[1].content).toBe("In progress task 2")
48+
expect(result[1].status).toBe("in_progress")
49+
})
50+
51+
it("should parse in-progress tasks with tilde", () => {
52+
const md = `[~] In progress task 1
53+
[~] In progress task 2`
54+
const result = parseMarkdownChecklist(md)
55+
expect(result).toHaveLength(2)
56+
expect(result[0].content).toBe("In progress task 1")
57+
expect(result[0].status).toBe("in_progress")
58+
expect(result[1].content).toBe("In progress task 2")
59+
expect(result[1].status).toBe("in_progress")
60+
})
61+
})
62+
63+
describe("dash-prefixed checkbox format", () => {
64+
it("should parse pending tasks with dash prefix", () => {
65+
const md = `- [ ] Task 1
66+
- [ ] Task 2`
67+
const result = parseMarkdownChecklist(md)
68+
expect(result).toHaveLength(2)
69+
expect(result[0].content).toBe("Task 1")
70+
expect(result[0].status).toBe("pending")
71+
expect(result[1].content).toBe("Task 2")
72+
expect(result[1].status).toBe("pending")
73+
})
74+
75+
it("should parse completed tasks with dash prefix and lowercase x", () => {
76+
const md = `- [x] Completed task 1
77+
- [x] Completed task 2`
78+
const result = parseMarkdownChecklist(md)
79+
expect(result).toHaveLength(2)
80+
expect(result[0].content).toBe("Completed task 1")
81+
expect(result[0].status).toBe("completed")
82+
expect(result[1].content).toBe("Completed task 2")
83+
expect(result[1].status).toBe("completed")
84+
})
85+
86+
it("should parse completed tasks with dash prefix and uppercase X", () => {
87+
const md = `- [X] Completed task 1
88+
- [X] Completed task 2`
89+
const result = parseMarkdownChecklist(md)
90+
expect(result).toHaveLength(2)
91+
expect(result[0].content).toBe("Completed task 1")
92+
expect(result[0].status).toBe("completed")
93+
expect(result[1].content).toBe("Completed task 2")
94+
expect(result[1].status).toBe("completed")
95+
})
96+
97+
it("should parse in-progress tasks with dash prefix and dash marker", () => {
98+
const md = `- [-] In progress task 1
99+
- [-] In progress task 2`
100+
const result = parseMarkdownChecklist(md)
101+
expect(result).toHaveLength(2)
102+
expect(result[0].content).toBe("In progress task 1")
103+
expect(result[0].status).toBe("in_progress")
104+
expect(result[1].content).toBe("In progress task 2")
105+
expect(result[1].status).toBe("in_progress")
106+
})
107+
108+
it("should parse in-progress tasks with dash prefix and tilde marker", () => {
109+
const md = `- [~] In progress task 1
110+
- [~] In progress task 2`
111+
const result = parseMarkdownChecklist(md)
112+
expect(result).toHaveLength(2)
113+
expect(result[0].content).toBe("In progress task 1")
114+
expect(result[0].status).toBe("in_progress")
115+
expect(result[1].content).toBe("In progress task 2")
116+
expect(result[1].status).toBe("in_progress")
117+
})
118+
})
119+
120+
describe("mixed formats", () => {
121+
it("should parse mixed formats correctly", () => {
122+
const md = `[ ] Task without dash
123+
- [ ] Task with dash
124+
[x] Completed without dash
125+
- [X] Completed with dash
126+
[-] In progress without dash
127+
- [~] In progress with dash`
128+
const result = parseMarkdownChecklist(md)
129+
expect(result).toHaveLength(6)
130+
131+
expect(result[0].content).toBe("Task without dash")
132+
expect(result[0].status).toBe("pending")
133+
134+
expect(result[1].content).toBe("Task with dash")
135+
expect(result[1].status).toBe("pending")
136+
137+
expect(result[2].content).toBe("Completed without dash")
138+
expect(result[2].status).toBe("completed")
139+
140+
expect(result[3].content).toBe("Completed with dash")
141+
expect(result[3].status).toBe("completed")
142+
143+
expect(result[4].content).toBe("In progress without dash")
144+
expect(result[4].status).toBe("in_progress")
145+
146+
expect(result[5].content).toBe("In progress with dash")
147+
expect(result[5].status).toBe("in_progress")
148+
})
149+
})
150+
151+
describe("edge cases", () => {
152+
it("should handle empty strings", () => {
153+
const result = parseMarkdownChecklist("")
154+
expect(result).toEqual([])
155+
})
156+
157+
it("should handle non-string input", () => {
158+
const result = parseMarkdownChecklist(null as any)
159+
expect(result).toEqual([])
160+
})
161+
162+
it("should handle undefined input", () => {
163+
const result = parseMarkdownChecklist(undefined as any)
164+
expect(result).toEqual([])
165+
})
166+
167+
it("should ignore non-checklist lines", () => {
168+
const md = `This is not a checklist
169+
[ ] Valid task
170+
Just some text
171+
- Not a checklist item
172+
- [x] Valid completed task
173+
[not valid] Invalid format`
174+
const result = parseMarkdownChecklist(md)
175+
expect(result).toHaveLength(2)
176+
expect(result[0].content).toBe("Valid task")
177+
expect(result[0].status).toBe("pending")
178+
expect(result[1].content).toBe("Valid completed task")
179+
expect(result[1].status).toBe("completed")
180+
})
181+
182+
it("should handle extra spaces", () => {
183+
const md = ` [ ] Task with spaces
184+
- [ ] Task with dash and spaces
185+
[x] Completed with spaces
186+
- [X] Completed with dash and spaces`
187+
const result = parseMarkdownChecklist(md)
188+
expect(result).toHaveLength(4)
189+
expect(result[0].content).toBe("Task with spaces")
190+
expect(result[1].content).toBe("Task with dash and spaces")
191+
expect(result[2].content).toBe("Completed with spaces")
192+
expect(result[3].content).toBe("Completed with dash and spaces")
193+
})
194+
195+
it("should handle Windows line endings", () => {
196+
const md = "[ ] Task 1\r\n- [x] Task 2\r\n[-] Task 3"
197+
const result = parseMarkdownChecklist(md)
198+
expect(result).toHaveLength(3)
199+
expect(result[0].content).toBe("Task 1")
200+
expect(result[0].status).toBe("pending")
201+
expect(result[1].content).toBe("Task 2")
202+
expect(result[1].status).toBe("completed")
203+
expect(result[2].content).toBe("Task 3")
204+
expect(result[2].status).toBe("in_progress")
205+
})
206+
})
207+
208+
describe("ID generation", () => {
209+
it("should generate consistent IDs for the same content and status", () => {
210+
const md1 = `[ ] Task 1
211+
[x] Task 2`
212+
const md2 = `[ ] Task 1
213+
[x] Task 2`
214+
const result1 = parseMarkdownChecklist(md1)
215+
const result2 = parseMarkdownChecklist(md2)
216+
217+
expect(result1[0].id).toBe(result2[0].id)
218+
expect(result1[1].id).toBe(result2[1].id)
219+
})
220+
221+
it("should generate different IDs for different content", () => {
222+
const md = `[ ] Task 1
223+
[ ] Task 2`
224+
const result = parseMarkdownChecklist(md)
225+
expect(result[0].id).not.toBe(result[1].id)
226+
})
227+
228+
it("should generate different IDs for same content but different status", () => {
229+
const md = `[ ] Task 1
230+
[x] Task 1`
231+
const result = parseMarkdownChecklist(md)
232+
expect(result[0].id).not.toBe(result[1].id)
233+
})
234+
235+
it("should generate same IDs regardless of dash prefix", () => {
236+
const md1 = `[ ] Task 1`
237+
const md2 = `- [ ] Task 1`
238+
const result1 = parseMarkdownChecklist(md1)
239+
const result2 = parseMarkdownChecklist(md2)
240+
expect(result1[0].id).toBe(result2[0].id)
241+
})
242+
})
243+
})

src/core/tools/updateTodoListTool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ export function parseMarkdownChecklist(md: string): TodoItem[] {
108108
.filter(Boolean)
109109
const todos: TodoItem[] = []
110110
for (const line of lines) {
111-
const match = line.match(/^\[\s*([ xX\-~])\s*\]\s+(.+)$/)
111+
// Support both "[ ] Task" and "- [ ] Task" formats
112+
const match = line.match(/^(?:-\s*)?\[\s*([ xX\-~])\s*\]\s+(.+)$/)
112113
if (!match) continue
113114
let status: TodoStatus = "pending"
114115
if (match[1] === "x" || match[1] === "X") status = "completed"

0 commit comments

Comments
 (0)