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;
}