Skip to content

fix: restore list styles for markdown lists in chat interface #6095

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions apps/vscode-e2e/src/suite/markdown-lists.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
25 changes: 25 additions & 0 deletions webview-ui/src/components/common/MarkdownBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<MarkdownBlock markdown={markdown} />)

// 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(<MarkdownBlock markdown={markdown} />)

// 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(<MarkdownBlock markdown={markdown} />)

// 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()
})
})
25 changes: 25 additions & 0 deletions webview-ui/src/components/settings/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down