Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
155ac1c
fix(webview): render expanded task header text as markdown
easonliang28 Aug 16, 2026
c90f28d
fix(webview): use VS Code-style scrollbar for expanded task prompt box
easonliang28 Aug 16, 2026
0f4deba
fix(webview): keep task header expanded when clicking rendered markdo…
easonliang28 Aug 16, 2026
73ed937
test(webview): drop as-any cast from empty-prompt TaskHeader fixture
easonliang28 Aug 16, 2026
0842e9b
Merge branch 'main' into fix/task-header-markdown
easonLiangWorldedtech Aug 17, 2026
1d13c74
fix(chat): preserve mentions in expanded task markdown
easonliang28 Aug 17, 2026
3811f0b
Merge branch 'main' into fix/task-header-markdown
easonLiangWorldedtech Aug 19, 2026
99986b3
fix(webview): keep expanded task panel open after mention click
easonliang28 Aug 19, 2026
03e98ab
fix(webview): keep mentions literal in code and keyboard accessible
easonliang28 Aug 19, 2026
279be66
test(webview): cover remaining mention-splitter branches
easonliang28 Aug 19, 2026
84b22e3
fix(webview): gate clickable mentions to user-authored task text
easonliang28 Aug 20, 2026
1206a95
Merge branch 'main' into fix/task-header-markdown
easonLiangWorldedtech Aug 20, 2026
bd93019
fix(webview): keep newlines and full mention paths in expanded task h…
easonliang28 Aug 22, 2026
630309c
fix(webview): respect mention boundaries and prevent Space scroll on …
easonliang28 Aug 22, 2026
d9bc16d
fix(webview): mask reference link definitions before mention rewriting
easonliang28 Aug 23, 2026
203f76b
fix(webview): mask reference links and image references before mentio…
easonliang28 Aug 23, 2026
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"
Comment thread
easonLiangWorldedtech marked this conversation as resolved.
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.
Outdated
Comment thread
easonLiangWorldedtech marked this conversation as resolved.
Outdated
</div>
</div>
{task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
Expand Down
89 changes: 89 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,93 @@ 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({
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()
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
})
Loading