diff --git a/apps/vscode-e2e/src/suite/markdown-lists.test.ts b/apps/vscode-e2e/src/suite/markdown-lists.test.ts new file mode 100644 index 00000000000..a229d9c2700 --- /dev/null +++ b/apps/vscode-e2e/src/suite/markdown-lists.test.ts @@ -0,0 +1,168 @@ +import * as assert from "assert" + +import type { ClineMessage } from "@roo-code/types" + +import { waitUntilCompleted } from "./utils" +import { setDefaultSuiteTimeout } from "./test-utils" + +suite("Markdown List Rendering", function () { + setDefaultSuiteTimeout(this) + + test("Should render unordered lists with bullets in chat", async () => { + const api = globalThis.api + + const messages: ClineMessage[] = [] + + api.on("message", ({ message }: { message: ClineMessage }) => { + if (message.type === "say" && message.partial === false) { + messages.push(message) + } + }) + + const taskId = await api.startNewTask({ + configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, + text: "Please show me an example of an unordered list with the following items: Apple, Banana, Orange", + }) + + await waitUntilCompleted({ api, taskId }) + + // Find the message containing the list + const listMessage = messages.find( + ({ say, text }) => + (say === "completion_result" || say === "text") && + text?.includes("Apple") && + text?.includes("Banana") && + text?.includes("Orange"), + ) + + assert.ok(listMessage, "Should have a message containing the list items") + + // The rendered markdown should contain list markers + const messageText = listMessage?.text || "" + assert.ok( + messageText.includes("- Apple") || messageText.includes("* Apple") || messageText.includes("• Apple"), + "List items should be rendered with bullet points", + ) + }) + + test("Should render ordered lists with numbers in chat", async () => { + const api = globalThis.api + + const messages: ClineMessage[] = [] + + api.on("message", ({ message }: { message: ClineMessage }) => { + if (message.type === "say" && message.partial === false) { + messages.push(message) + } + }) + + const taskId = await api.startNewTask({ + configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, + text: "Please show me a numbered list with three steps: First step, Second step, Third step", + }) + + await waitUntilCompleted({ api, taskId }) + + // Find the message containing the numbered list + const listMessage = messages.find( + ({ say, text }) => + (say === "completion_result" || say === "text") && + text?.includes("First step") && + text?.includes("Second step") && + text?.includes("Third step"), + ) + + assert.ok(listMessage, "Should have a message containing the numbered list") + + // The rendered markdown should contain numbered markers + const messageText = listMessage?.text || "" + assert.ok( + messageText.includes("1. First step") || messageText.includes("1) First step"), + "List items should be rendered with numbers", + ) + }) + + test("Should render nested lists with proper hierarchy", async () => { + const api = globalThis.api + + const messages: ClineMessage[] = [] + + api.on("message", ({ message }: { message: ClineMessage }) => { + if (message.type === "say" && message.partial === false) { + messages.push(message) + } + }) + + const taskId = await api.startNewTask({ + configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, + text: "Please create a nested list with 'Main item' having two sub-items: 'Sub-item A' and 'Sub-item B'", + }) + + await waitUntilCompleted({ api, taskId }) + + // Find the message containing the nested list + const listMessage = messages.find( + ({ say, text }) => + (say === "completion_result" || say === "text") && + text?.includes("Main item") && + text?.includes("Sub-item A") && + text?.includes("Sub-item B"), + ) + + assert.ok(listMessage, "Should have a message containing the nested list") + + // The rendered markdown should show hierarchy through indentation + const messageText = listMessage?.text || "" + + // Check for main item + assert.ok( + messageText.includes("- Main item") || + messageText.includes("* Main item") || + messageText.includes("• Main item"), + "Main list item should be rendered", + ) + + // Check for sub-items with indentation (typically 2-4 spaces or a tab) + assert.ok( + messageText.match(/\s{2,}- Sub-item A/) || + messageText.match(/\s{2,}\* Sub-item A/) || + messageText.match(/\s{2,}• Sub-item A/) || + messageText.includes("\t- Sub-item A") || + messageText.includes("\t* Sub-item A") || + messageText.includes("\t• Sub-item A"), + "Sub-items should be indented", + ) + }) + + test("Should render mixed ordered and unordered lists", async () => { + const api = globalThis.api + + const messages: ClineMessage[] = [] + + api.on("message", ({ message }: { message: ClineMessage }) => { + if (message.type === "say" && message.partial === false) { + messages.push(message) + } + }) + + const taskId = await api.startNewTask({ + configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, + text: "Please create a list that has both numbered items and bullet points, mixing ordered and unordered lists", + }) + + await waitUntilCompleted({ api, taskId }) + + // Find a message that contains both types of lists + const listMessage = messages.find( + ({ say, text }) => + (say === "completion_result" || say === "text") && + text && + // Check for numbered list markers + (text.includes("1.") || text.includes("1)")) && + // Check for bullet list markers + (text.includes("-") || text.includes("*") || text.includes("•")), + ) + + assert.ok(listMessage, "Should have a message containing mixed list types") + }) +}) diff --git a/webview-ui/src/components/common/MarkdownBlock.tsx b/webview-ui/src/components/common/MarkdownBlock.tsx index fe033efe3fb..4c958593aaf 100644 --- a/webview-ui/src/components/common/MarkdownBlock.tsx +++ b/webview-ui/src/components/common/MarkdownBlock.tsx @@ -151,6 +151,31 @@ const StyledMarkdown = styled.div` margin-left: 0; } + ol { + list-style-type: decimal; + } + + ul { + list-style-type: disc; + } + + /* Nested list styles */ + ul ul { + list-style-type: circle; + } + + ul ul ul { + list-style-type: square; + } + + ol ol { + list-style-type: lower-alpha; + } + + ol ol ol { + list-style-type: lower-roman; + } + p { white-space: pre-wrap; } diff --git a/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx b/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx index ec97e4e6676..38a0680b229 100644 --- a/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx +++ b/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx @@ -36,4 +36,84 @@ describe("MarkdownBlock", () => { const paragraph = container.querySelector("p") expect(paragraph?.textContent).toBe("Check out this link: https://example.com.") }) + + it("should render unordered lists with proper styling", async () => { + const markdown = `Here are some items: +- First item +- Second item + - Nested item + - Another nested item` + + const { container } = render() + + // Wait for the content to be processed + await screen.findByText(/Here are some items/, { exact: false }) + + // Check that ul elements exist + const ulElements = container.querySelectorAll("ul") + expect(ulElements.length).toBeGreaterThan(0) + + // Check that list items exist + const liElements = container.querySelectorAll("li") + expect(liElements.length).toBe(4) + + // Verify the text content + expect(screen.getByText("First item")).toBeInTheDocument() + expect(screen.getByText("Second item")).toBeInTheDocument() + expect(screen.getByText("Nested item")).toBeInTheDocument() + expect(screen.getByText("Another nested item")).toBeInTheDocument() + }) + + it("should render ordered lists with proper styling", async () => { + const markdown = `And a numbered list: +1. Step one +2. Step two +3. Step three` + + const { container } = render() + + // Wait for the content to be processed + await screen.findByText(/And a numbered list/, { exact: false }) + + // Check that ol elements exist + const olElements = container.querySelectorAll("ol") + expect(olElements.length).toBe(1) + + // Check that list items exist + const liElements = container.querySelectorAll("li") + expect(liElements.length).toBe(3) + + // Verify the text content + expect(screen.getByText("Step one")).toBeInTheDocument() + expect(screen.getByText("Step two")).toBeInTheDocument() + expect(screen.getByText("Step three")).toBeInTheDocument() + }) + + it("should render nested lists with proper hierarchy", async () => { + const markdown = `Complex list: +1. First level ordered + - Second level unordered + - Another second level + 1. Third level ordered + 2. Another third level +2. Back to first level` + + const { container } = render() + + // Wait for the content to be processed + await screen.findByText(/Complex list/, { exact: false }) + + // Check nested structure + const olElements = container.querySelectorAll("ol") + const ulElements = container.querySelectorAll("ul") + + expect(olElements.length).toBeGreaterThan(0) + expect(ulElements.length).toBeGreaterThan(0) + + // Verify all text is rendered + expect(screen.getByText("First level ordered")).toBeInTheDocument() + expect(screen.getByText("Second level unordered")).toBeInTheDocument() + expect(screen.getByText("Third level ordered")).toBeInTheDocument() + expect(screen.getByText("Back to first level")).toBeInTheDocument() + }) }) diff --git a/webview-ui/src/components/settings/styles.ts b/webview-ui/src/components/settings/styles.ts index 7eac289bb0f..8d123f8d41e 100644 --- a/webview-ui/src/components/settings/styles.ts +++ b/webview-ui/src/components/settings/styles.ts @@ -32,6 +32,31 @@ export const StyledMarkdown = styled.div` margin-left: 0; } + ol { + list-style-type: decimal; + } + + ul { + list-style-type: disc; + } + + /* Nested list styles */ + ul ul { + list-style-type: circle; + } + + ul ul ul { + list-style-type: square; + } + + ol ol { + list-style-type: lower-alpha; + } + + ol ol ol { + list-style-type: lower-roman; + } + p { white-space: pre-wrap; }