Skip to content
Open
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
10 changes: 7 additions & 3 deletions webview-ui/src/components/chat/TaskHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -324,13 +328,13 @@ const TaskHeader = ({
className="text-vscode-font-size overflow-y-auto break-words break-anywhere relative">
<div
ref={textRef}
className="overflow-auto max-h-80 whitespace-pre-wrap break-words break-anywhere cursor-text py-0.5"
className="scrollable overflow-auto max-h-80 break-words break-anywhere cursor-text py-0.5"
style={{
display: "-webkit-box",
WebkitLineClamp: "unset",
WebkitBoxOrient: "vertical",
}}>
<Mention text={task.text} />
<MarkdownBlock markdown={task.text ?? ""} />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
</div>
{task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
Expand Down
90 changes: 90 additions & 0 deletions webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,94 @@ describe("TaskHeader", () => {
expect(screen.getByText("25%")).toBeInTheDocument()
})
})

describe("Expanded task text markdown rendering", () => {
it("shows raw source while collapsed and formatted markdown when expanded", async () => {
const { container } = renderTaskHeader({
task: { type: "say", ts: Date.now(), text: "**bold** and `code`", images: [] },
})

// Collapsed state renders the raw task text (no markdown formatting yet).
expect(screen.getByText("**bold** and `code`")).toBeInTheDocument()
expect(container.querySelector("strong")).toBeNull()

// Expand the header by clicking the collapsed title.
fireEvent.click(screen.getByText("**bold** and `code`"))

// Expanded state applies markdown: **bold** becomes <strong>, `code` becomes <code>.
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()
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
})
Loading