Skip to content

Commit 3a19b16

Browse files
msukkariclaude
andcommitted
fix(web): preserve angle-bracketed text in chat user questions
Add a remarkPreserveHtml remark plugin that converts HTML MDAST nodes to text nodes before the references plugin runs. This prevents text like <id> from being parsed as HTML tags and stripped by sanitization, while keeping @file:{...} reference rendering intact. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3d37703 commit 3a19b16

File tree

2 files changed

+30
-2
lines changed

2 files changed

+30
-2
lines changed

packages/web/src/features/chat/components/chatThread/chatThreadListItem.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
334334
<MarkdownRenderer
335335
content={userQuestion.trim()}
336336
className="prose-p:m-0"
337+
escapeHtml={true}
337338
/>
338339
</div>
339340

packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,27 @@ function remarkReferencesPlugin() {
8080
}
8181
}
8282

83+
/**
84+
* A remark plugin that converts `html` MDAST nodes into `text` nodes,
85+
* preserving angle-bracketed content like `<id>` as visible text. Without this,
86+
* `<id>` is parsed as an HTML tag and then stripped by sanitization.
87+
*
88+
* This plugin must run BEFORE remarkReferencesPlugin so that the file-reference
89+
* HTML nodes created by that plugin are left intact for rehypeRaw to process.
90+
*/
91+
function remarkPreserveHtml() {
92+
return function (tree: Nodes) {
93+
visit(tree, 'html', (node, index, parent) => {
94+
if (index !== undefined && parent && 'children' in parent) {
95+
(parent.children as Nodes[])[index] = {
96+
type: 'text',
97+
value: (node as { value: string }).value,
98+
};
99+
}
100+
});
101+
};
102+
}
103+
83104
const remarkTocExtractor = () => {
84105
return function (tree: Nodes) {
85106
visit(tree, 'heading', (node: Heading) => {
@@ -101,18 +122,24 @@ const remarkTocExtractor = () => {
101122
interface MarkdownRendererProps {
102123
content: string;
103124
className?: string;
125+
/**
126+
* When true, angle-bracketed text like `<id>` is preserved as visible text
127+
* instead of being parsed as HTML. File references (@file:{...}) are unaffected.
128+
*/
129+
escapeHtml?: boolean;
104130
}
105131

106-
const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererProps>(({ content, className }, ref) => {
132+
const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererProps>(({ content, className, escapeHtml = false }, ref) => {
107133
const router = useRouter();
108134

109135
const remarkPlugins = useMemo((): PluggableList => {
110136
return [
111137
remarkGfm,
138+
...(escapeHtml ? [remarkPreserveHtml] : []),
112139
remarkReferencesPlugin,
113140
remarkTocExtractor,
114141
];
115-
}, []);
142+
}, [escapeHtml]);
116143

117144
const rehypePlugins = useMemo((): PluggableList => {
118145
return [

0 commit comments

Comments
 (0)