Просмотр исходного кода

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]>
roomote[bot] 5 месяцев назад
Родитель
Сommit
c61bd95bbe

+ 168 - 0
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")
+	})
+})

+ 25 - 0
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;
 	}

+ 80 - 0
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(<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 - 0
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;
 	}