diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 0941a22e2b..b614dbbb92 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -29,6 +29,8 @@ import { Mention } from "./Mention" import { TodoListDisplay } from "./TodoListDisplay" import { LucideIconButton } from "./LucideIconButton" +import MarkdownBlock from "../common/MarkdownBlock" + export interface TaskHeaderProps { task: ClineMessage tokensIn: number @@ -163,7 +165,9 @@ const TaskHeader = ({ e.target.closest('[role="button"]') || e.target.closest("[data-radix-popper-content-wrapper]") || e.target.closest("img") || - e.target.tagName === "IMG") + e.target.tagName === "IMG" || + e.target.closest("a") || + e.target.tagName === "A") ) { return } @@ -324,13 +328,13 @@ const TaskHeader = ({ className="text-vscode-font-size overflow-y-auto break-words break-anywhere relative">
.
+ const bold = await screen.findByText("bold")
+ expect(bold.tagName).toBe("STRONG")
+ expect(container.querySelector("code")?.textContent).toBe("code")
+
+ // The raw markdown source must not be displayed verbatim in the expanded view.
+ expect(screen.queryByText("**bold** and `code`")).not.toBeInTheDocument()
+ })
+
+ it("uses the shared scrollable style for the expanded prompt box", () => {
+ const { container } = renderTaskHeader({
+ task: { type: "say", ts: Date.now(), text: "prompt", images: [] },
+ })
+
+ // Expand the header.
+ fireEvent.click(screen.getByText("prompt"))
+
+ // The prompt box must use the VS Code-style .scrollable scrollbar (hover-reveal),
+ // not a default always-visible Chromium scrollbar, so it matches the message list.
+ const scrollBox = container.querySelector(".scrollable")
+ expect(scrollBox).not.toBeNull()
+ expect(scrollBox?.className).toContain("max-h-80")
+ })
+
+ it("renders headings and lists in the expanded view", async () => {
+ const { container } = renderTaskHeader({
+ task: {
+ type: "say",
+ ts: Date.now(),
+ text: "# Heading\n- item one\n- item two",
+ images: [],
+ },
+ })
+
+ // Expand via the header container (the raw multi-line title is not a stable text target).
+ fireEvent.click(container.querySelector(".cursor-pointer")!)
+
+ const heading = await screen.findByRole("heading")
+ expect(heading.textContent).toBe("Heading")
+ expect(container.querySelector("ul li")).not.toBeNull()
+ })
+
+ it("does not collapse the panel when a rendered markdown link is clicked", async () => {
+ const { container } = renderTaskHeader({
+ task: {
+ type: "say",
+ ts: Date.now(),
+ text: "**bold** [example](https://example.com)",
+ images: [],
+ },
+ })
+
+ // Expand the header.
+ fireEvent.click(screen.getByText("**bold** [example](https://example.com)"))
+ const link = await screen.findByRole("link", { name: "example" })
+
+ // Clicking a rendered link must not toggle isTaskExpanded (the header click
+ // handler ignores anchor targets), so the expanded content stays visible.
+ fireEvent.click(link)
+ expect(container.querySelector("strong")).not.toBeNull()
+ })
+
+ it("renders an empty prompt without crashing", () => {
+ const { container } = renderTaskHeader({
+ // `text` is optional on ClineMessage; omit it to exercise the empty-prompt path.
+ task: { type: "say", ts: Date.now(), images: [] },
+ })
+
+ // No title text to click, so expand via the header container itself.
+ fireEvent.click(container.querySelector(".cursor-pointer")!)
+
+ // The empty prompt renders nothing but must not crash; the rest of the
+ // expanded header (cost row) is still present.
+ expect(screen.getByText("$0.05")).toBeInTheDocument()
+ })
+ })
})