Skip to content

Commit c61bd95

Browse files
roomote[bot]roomotedaniel-lxs
authored
fix: restore list styles for markdown lists in chat interface (#6095)
Co-authored-by: Roo Code <[email protected]> Co-authored-by: Daniel Riccio <[email protected]>
1 parent c68f8f5 commit c61bd95

File tree

4 files changed

+298
-0
lines changed

4 files changed

+298
-0
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import * as assert from "assert"
2+
3+
import type { ClineMessage } from "@roo-code/types"
4+
5+
import { waitUntilCompleted } from "./utils"
6+
import { setDefaultSuiteTimeout } from "./test-utils"
7+
8+
suite("Markdown List Rendering", function () {
9+
setDefaultSuiteTimeout(this)
10+
11+
test("Should render unordered lists with bullets in chat", async () => {
12+
const api = globalThis.api
13+
14+
const messages: ClineMessage[] = []
15+
16+
api.on("message", ({ message }: { message: ClineMessage }) => {
17+
if (message.type === "say" && message.partial === false) {
18+
messages.push(message)
19+
}
20+
})
21+
22+
const taskId = await api.startNewTask({
23+
configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true },
24+
text: "Please show me an example of an unordered list with the following items: Apple, Banana, Orange",
25+
})
26+
27+
await waitUntilCompleted({ api, taskId })
28+
29+
// Find the message containing the list
30+
const listMessage = messages.find(
31+
({ say, text }) =>
32+
(say === "completion_result" || say === "text") &&
33+
text?.includes("Apple") &&
34+
text?.includes("Banana") &&
35+
text?.includes("Orange"),
36+
)
37+
38+
assert.ok(listMessage, "Should have a message containing the list items")
39+
40+
// The rendered markdown should contain list markers
41+
const messageText = listMessage?.text || ""
42+
assert.ok(
43+
messageText.includes("- Apple") || messageText.includes("* Apple") || messageText.includes("• Apple"),
44+
"List items should be rendered with bullet points",
45+
)
46+
})
47+
48+
test("Should render ordered lists with numbers in chat", async () => {
49+
const api = globalThis.api
50+
51+
const messages: ClineMessage[] = []
52+
53+
api.on("message", ({ message }: { message: ClineMessage }) => {
54+
if (message.type === "say" && message.partial === false) {
55+
messages.push(message)
56+
}
57+
})
58+
59+
const taskId = await api.startNewTask({
60+
configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true },
61+
text: "Please show me a numbered list with three steps: First step, Second step, Third step",
62+
})
63+
64+
await waitUntilCompleted({ api, taskId })
65+
66+
// Find the message containing the numbered list
67+
const listMessage = messages.find(
68+
({ say, text }) =>
69+
(say === "completion_result" || say === "text") &&
70+
text?.includes("First step") &&
71+
text?.includes("Second step") &&
72+
text?.includes("Third step"),
73+
)
74+
75+
assert.ok(listMessage, "Should have a message containing the numbered list")
76+
77+
// The rendered markdown should contain numbered markers
78+
const messageText = listMessage?.text || ""
79+
assert.ok(
80+
messageText.includes("1. First step") || messageText.includes("1) First step"),
81+
"List items should be rendered with numbers",
82+
)
83+
})
84+
85+
test("Should render nested lists with proper hierarchy", async () => {
86+
const api = globalThis.api
87+
88+
const messages: ClineMessage[] = []
89+
90+
api.on("message", ({ message }: { message: ClineMessage }) => {
91+
if (message.type === "say" && message.partial === false) {
92+
messages.push(message)
93+
}
94+
})
95+
96+
const taskId = await api.startNewTask({
97+
configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true },
98+
text: "Please create a nested list with 'Main item' having two sub-items: 'Sub-item A' and 'Sub-item B'",
99+
})
100+
101+
await waitUntilCompleted({ api, taskId })
102+
103+
// Find the message containing the nested list
104+
const listMessage = messages.find(
105+
({ say, text }) =>
106+
(say === "completion_result" || say === "text") &&
107+
text?.includes("Main item") &&
108+
text?.includes("Sub-item A") &&
109+
text?.includes("Sub-item B"),
110+
)
111+
112+
assert.ok(listMessage, "Should have a message containing the nested list")
113+
114+
// The rendered markdown should show hierarchy through indentation
115+
const messageText = listMessage?.text || ""
116+
117+
// Check for main item
118+
assert.ok(
119+
messageText.includes("- Main item") ||
120+
messageText.includes("* Main item") ||
121+
messageText.includes("• Main item"),
122+
"Main list item should be rendered",
123+
)
124+
125+
// Check for sub-items with indentation (typically 2-4 spaces or a tab)
126+
assert.ok(
127+
messageText.match(/\s{2,}- Sub-item A/) ||
128+
messageText.match(/\s{2,}\* Sub-item A/) ||
129+
messageText.match(/\s{2,} Sub-item A/) ||
130+
messageText.includes("\t- Sub-item A") ||
131+
messageText.includes("\t* Sub-item A") ||
132+
messageText.includes("\t• Sub-item A"),
133+
"Sub-items should be indented",
134+
)
135+
})
136+
137+
test("Should render mixed ordered and unordered lists", async () => {
138+
const api = globalThis.api
139+
140+
const messages: ClineMessage[] = []
141+
142+
api.on("message", ({ message }: { message: ClineMessage }) => {
143+
if (message.type === "say" && message.partial === false) {
144+
messages.push(message)
145+
}
146+
})
147+
148+
const taskId = await api.startNewTask({
149+
configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true },
150+
text: "Please create a list that has both numbered items and bullet points, mixing ordered and unordered lists",
151+
})
152+
153+
await waitUntilCompleted({ api, taskId })
154+
155+
// Find a message that contains both types of lists
156+
const listMessage = messages.find(
157+
({ say, text }) =>
158+
(say === "completion_result" || say === "text") &&
159+
text &&
160+
// Check for numbered list markers
161+
(text.includes("1.") || text.includes("1)")) &&
162+
// Check for bullet list markers
163+
(text.includes("-") || text.includes("*") || text.includes("•")),
164+
)
165+
166+
assert.ok(listMessage, "Should have a message containing mixed list types")
167+
})
168+
})

webview-ui/src/components/common/MarkdownBlock.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,31 @@ const StyledMarkdown = styled.div`
151151
margin-left: 0;
152152
}
153153
154+
ol {
155+
list-style-type: decimal;
156+
}
157+
158+
ul {
159+
list-style-type: disc;
160+
}
161+
162+
/* Nested list styles */
163+
ul ul {
164+
list-style-type: circle;
165+
}
166+
167+
ul ul ul {
168+
list-style-type: square;
169+
}
170+
171+
ol ol {
172+
list-style-type: lower-alpha;
173+
}
174+
175+
ol ol ol {
176+
list-style-type: lower-roman;
177+
}
178+
154179
p {
155180
white-space: pre-wrap;
156181
}

webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,84 @@ describe("MarkdownBlock", () => {
3636
const paragraph = container.querySelector("p")
3737
expect(paragraph?.textContent).toBe("Check out this link: https://example.com.")
3838
})
39+
40+
it("should render unordered lists with proper styling", async () => {
41+
const markdown = `Here are some items:
42+
- First item
43+
- Second item
44+
- Nested item
45+
- Another nested item`
46+
47+
const { container } = render(<MarkdownBlock markdown={markdown} />)
48+
49+
// Wait for the content to be processed
50+
await screen.findByText(/Here are some items/, { exact: false })
51+
52+
// Check that ul elements exist
53+
const ulElements = container.querySelectorAll("ul")
54+
expect(ulElements.length).toBeGreaterThan(0)
55+
56+
// Check that list items exist
57+
const liElements = container.querySelectorAll("li")
58+
expect(liElements.length).toBe(4)
59+
60+
// Verify the text content
61+
expect(screen.getByText("First item")).toBeInTheDocument()
62+
expect(screen.getByText("Second item")).toBeInTheDocument()
63+
expect(screen.getByText("Nested item")).toBeInTheDocument()
64+
expect(screen.getByText("Another nested item")).toBeInTheDocument()
65+
})
66+
67+
it("should render ordered lists with proper styling", async () => {
68+
const markdown = `And a numbered list:
69+
1. Step one
70+
2. Step two
71+
3. Step three`
72+
73+
const { container } = render(<MarkdownBlock markdown={markdown} />)
74+
75+
// Wait for the content to be processed
76+
await screen.findByText(/And a numbered list/, { exact: false })
77+
78+
// Check that ol elements exist
79+
const olElements = container.querySelectorAll("ol")
80+
expect(olElements.length).toBe(1)
81+
82+
// Check that list items exist
83+
const liElements = container.querySelectorAll("li")
84+
expect(liElements.length).toBe(3)
85+
86+
// Verify the text content
87+
expect(screen.getByText("Step one")).toBeInTheDocument()
88+
expect(screen.getByText("Step two")).toBeInTheDocument()
89+
expect(screen.getByText("Step three")).toBeInTheDocument()
90+
})
91+
92+
it("should render nested lists with proper hierarchy", async () => {
93+
const markdown = `Complex list:
94+
1. First level ordered
95+
- Second level unordered
96+
- Another second level
97+
1. Third level ordered
98+
2. Another third level
99+
2. Back to first level`
100+
101+
const { container } = render(<MarkdownBlock markdown={markdown} />)
102+
103+
// Wait for the content to be processed
104+
await screen.findByText(/Complex list/, { exact: false })
105+
106+
// Check nested structure
107+
const olElements = container.querySelectorAll("ol")
108+
const ulElements = container.querySelectorAll("ul")
109+
110+
expect(olElements.length).toBeGreaterThan(0)
111+
expect(ulElements.length).toBeGreaterThan(0)
112+
113+
// Verify all text is rendered
114+
expect(screen.getByText("First level ordered")).toBeInTheDocument()
115+
expect(screen.getByText("Second level unordered")).toBeInTheDocument()
116+
expect(screen.getByText("Third level ordered")).toBeInTheDocument()
117+
expect(screen.getByText("Back to first level")).toBeInTheDocument()
118+
})
39119
})

webview-ui/src/components/settings/styles.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,31 @@ export const StyledMarkdown = styled.div`
3232
margin-left: 0;
3333
}
3434
35+
ol {
36+
list-style-type: decimal;
37+
}
38+
39+
ul {
40+
list-style-type: disc;
41+
}
42+
43+
/* Nested list styles */
44+
ul ul {
45+
list-style-type: circle;
46+
}
47+
48+
ul ul ul {
49+
list-style-type: square;
50+
}
51+
52+
ol ol {
53+
list-style-type: lower-alpha;
54+
}
55+
56+
ol ol ol {
57+
list-style-type: lower-roman;
58+
}
59+
3560
p {
3661
white-space: pre-wrap;
3762
}

0 commit comments

Comments
 (0)