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
5 changes: 4 additions & 1 deletion apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ interface ChatMarkdownProps {
className?: string;
/** Treat single newlines as hard breaks — chat-style user input. */
lineBreaks?: boolean;
/** Parse sanitized raw HTML instead of displaying its source text. */
parseRawHtml?: boolean;
}

const EMPTY_MARKDOWN_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">> = [];
Expand Down Expand Up @@ -1256,6 +1258,7 @@ function ChatMarkdown({
skills = EMPTY_MARKDOWN_SKILLS,
className,
lineBreaks = false,
parseRawHtml = true,
}: ChatMarkdownProps) {
const { resolvedTheme } = useTheme();
const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, {
Expand Down Expand Up @@ -1566,7 +1569,7 @@ function ChatMarkdown({
remarkPlugins={
lineBreaks ? CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS : CHAT_MARKDOWN_REMARK_PLUGINS
}
rehypePlugins={CHAT_MARKDOWN_REHYPE_PLUGINS}
rehypePlugins={parseRawHtml ? CHAT_MARKDOWN_REHYPE_PLUGINS : undefined}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raw HTML still dropped for users

High Severity

When parseRawHtml is false, XML-like tags (e.g., <global-agent-instructions>) disappear from rendered markdown. This happens because setting rehypePlugins to undefined removes the plugin needed to process these html nodes, causing them to be dropped from the output.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6fc3bad. Configure here.

components={markdownComponents}
urlTransform={markdownUrlTransform}
>
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ function buildUserTimelineEntry(text: string) {
};
}

function buildAssistantTimelineEntry(text: string) {
const entry = buildUserTimelineEntry(text);
return {
...entry,
message: {
...entry.message,
role: "assistant" as const,
},
};
}

describe("MessagesTimeline", () => {
it("uses LegendList isNearEnd when deciding whether the live edge is visible", async () => {
const {
Expand Down Expand Up @@ -338,6 +349,38 @@ describe("MessagesTimeline", () => {
expect(markup).toContain('data-user-message-collapsible="false"');
});

it("preserves XML-like tags in rendered user messages", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
<MessagesTimeline
{...buildProps()}
timelineEntries={[
buildUserTimelineEntry(
"Without reading a file, do you have <global-agent-instructions> in your context?",
),
]}
/>,
);

expect(markup).toContain("do you have &lt;global-agent-instructions&gt; in your context?");
});

it("continues to render sanitized raw HTML in assistant messages", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
<MessagesTimeline
{...buildProps()}
timelineEntries={[
buildAssistantTimelineEntry("<details><summary>More</summary>Details</details>"),
]}
/>,
);

expect(markup).toContain('data-markdown-details=""');
expect(markup).toContain("More");
expect(markup).not.toContain("&lt;details&gt;");
});

it("renders inline terminal labels with the composer chip UI", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
skills={props.skills}
className="text-foreground"
lineBreaks
parseRawHtml={false}
/>
) : null}
{trailingWhitespace ? <span aria-hidden="true">{trailingWhitespace}</span> : null}
Expand All @@ -1509,6 +1510,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
skills={props.skills}
className="text-foreground"
lineBreaks
parseRawHtml={false}
/>
</div>
) : null
Expand Down Expand Up @@ -1597,6 +1599,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
skills={props.skills}
className="text-foreground"
lineBreaks
parseRawHtml={false}
/>,
);
} else if (inlinePrefix.length === 0) {
Expand All @@ -1622,6 +1625,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
skills={props.skills}
className="text-foreground"
lineBreaks
parseRawHtml={false}
/>
);
});
Expand Down
Loading