From 155ac1c5878ff005fb065ac1abf4a2d234c69afe Mon Sep 17 00:00:00 2001 From: Eason Liang Date: Sun, 16 Aug 2026 12:09:47 +0800 Subject: [PATCH 1/4] fix(webview): render expanded task header text as markdown The collapsed task title still shows raw text, but the expanded view rendered the prompt verbatim via , so markdown syntax (bold, code, lists) appeared as literal characters. Render it through MarkdownBlock like other chat messages and drop the now-redundant whitespace-pre-wrap class. --- webview-ui/src/components/chat/TaskHeader.tsx | 6 +++-- .../chat/__tests__/TaskHeader.spec.tsx | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 0941a22e2b..5140e45254 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 @@ -324,13 +326,13 @@ const TaskHeader = ({ className="text-vscode-font-size overflow-y-auto break-words break-anywhere relative">
- +
{task.images && task.images.length > 0 && } diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 2a302e6b18..76b9ecedbb 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -329,4 +329,27 @@ 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 , `code` becomes . + 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() + }) + }) }) From c90f28d2094fcf4529054971074a36ed5e3a044c Mon Sep 17 00:00:00 2001 From: Eason Liang Date: Sun, 16 Aug 2026 12:33:01 +0800 Subject: [PATCH 2/4] fix(webview): use VS Code-style scrollbar for expanded task prompt box The expanded prompt box used a default always-visible Chromium scrollbar while the message list uses the hover-reveal .scrollable style, so two differently-styled scrollbars stacked in the same column. Add the shared .scrollable class so both behave consistently. --- webview-ui/src/components/chat/TaskHeader.tsx | 2 +- .../components/chat/__tests__/TaskHeader.spec.tsx | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 5140e45254..5ff0e801d3 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -326,7 +326,7 @@ const TaskHeader = ({ className="text-vscode-font-size overflow-y-auto break-words break-anywhere relative">
{ // 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") + }) }) }) From 0f4debadaf6d10e25c9f6c013e49d6b240c013f9 Mon Sep 17 00:00:00 2001 From: Eason Liang Date: Sun, 16 Aug 2026 12:50:47 +0800 Subject: [PATCH 3/4] fix(webview): keep task header expanded when clicking rendered markdown links MarkdownBlock renders prompt links as elements, which the header click handler did not guard against (only buttons/role=button/img), so clicking a link inside the expanded prompt toggled isTaskExpanded and collapsed the panel. Ignore anchor targets in the toggle handler; add regression tests for link clicks, headings/lists rendering, and empty prompts. --- webview-ui/src/components/chat/TaskHeader.tsx | 4 +- .../chat/__tests__/TaskHeader.spec.tsx | 51 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 5ff0e801d3..b614dbbb92 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -165,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 } diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 633aa783d5..4d9bd40656 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -366,5 +366,56 @@ describe("TaskHeader", () => { 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({ + task: { type: "say", ts: Date.now(), text: undefined as any, 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() + }) }) }) From 73ed937e68f9137508fc167b51484a73c8765c4d Mon Sep 17 00:00:00 2001 From: Eason Liang Date: Sun, 16 Aug 2026 12:57:43 +0800 Subject: [PATCH 4/4] test(webview): drop as-any cast from empty-prompt TaskHeader fixture ClineMessage.text is optional (z.string().optional()), so the empty-prompt case can omit the property instead of casting undefined through any. --- webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 4d9bd40656..926762e93c 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -407,7 +407,8 @@ describe("TaskHeader", () => { it("renders an empty prompt without crashing", () => { const { container } = renderTaskHeader({ - task: { type: "say", ts: Date.now(), text: undefined as any, images: [] }, + // `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.