From 7a5964c7975cbd20fe5b6ddd9ae6243938f0d8b7 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 21 Mar 2026 19:01:25 +0000 Subject: [PATCH] fix: normalize literal \n escape sequences in parseMarkdownChecklist Some models/providers send the todos parameter with literal escape sequences (the two-character string \n) rather than actual newline characters. This causes all checklist items to end up on a single line, resulting in only one todo item being parsed. Normalize literal \n and \r\n sequences into actual newlines before splitting. Adds test coverage for this scenario. Closes #11977 --- src/core/tools/UpdateTodoListTool.ts | 5 +++- .../__tests__/updateTodoListTool.spec.ts | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/core/tools/UpdateTodoListTool.ts b/src/core/tools/UpdateTodoListTool.ts index 7414b713cf4..3f23ed51ed2 100644 --- a/src/core/tools/UpdateTodoListTool.ts +++ b/src/core/tools/UpdateTodoListTool.ts @@ -177,7 +177,10 @@ function normalizeStatus(status: string | undefined): TodoStatus { export function parseMarkdownChecklist(md: string): TodoItem[] { if (typeof md !== "string") return [] - const lines = md + // Normalize literal escape sequences (e.g. "\\n", "\\r\\n") that some + // models/providers send instead of actual newline characters. + const normalized = md.replace(/\\r\\n|\\n/g, "\n") + const lines = normalized .split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean) diff --git a/src/core/tools/__tests__/updateTodoListTool.spec.ts b/src/core/tools/__tests__/updateTodoListTool.spec.ts index ebe0500d665..6549090f817 100644 --- a/src/core/tools/__tests__/updateTodoListTool.spec.ts +++ b/src/core/tools/__tests__/updateTodoListTool.spec.ts @@ -205,6 +205,35 @@ Just some text }) }) + describe("literal escape sequence normalization", () => { + it("should parse items separated by literal \\n escape sequences", () => { + const md = "[ ] Task 1\\n[x] Task 2\\n[-] Task 3" + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(3) + expect(result[0].content).toBe("Task 1") + expect(result[0].status).toBe("pending") + expect(result[1].content).toBe("Task 2") + expect(result[1].status).toBe("completed") + expect(result[2].content).toBe("Task 3") + expect(result[2].status).toBe("in_progress") + }) + + it("should parse items separated by literal \\r\\n escape sequences", () => { + const md = "[ ] Task 1\\r\\n- [x] Task 2\\r\\n[~] Task 3" + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(3) + expect(result[0].content).toBe("Task 1") + expect(result[1].content).toBe("Task 2") + expect(result[2].content).toBe("Task 3") + }) + + it("should handle a mix of literal and actual newlines", () => { + const md = "[ ] Task 1\\n[x] Task 2\n[-] Task 3" + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(3) + }) + }) + describe("ID generation", () => { it("should generate consistent IDs for the same content and status", () => { const md1 = `[ ] Task 1