Skip to content

Commit d4354cc

Browse files
msukkariclaude
andauthored
fix(web): preserve angle-bracketed text in chat user questions (#932)
* Revert "Prompt angle bracket visibility (#929)" This reverts commit b4d7721. * 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> * docs: add changelog entry for angle bracket visibility fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: restore #929 reference in changelog entry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fb82717 commit d4354cc

File tree

3 files changed

+33
-19
lines changed

3 files changed

+33
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Hide version upgrade toast for askgithub deployment (`EXPERIMENT_ASK_GH_ENABLED`). [#931](https://github.com/sourcebot-dev/sourcebot/pull/931)
1616

1717
### Fixed
18-
- Fixed text inside angle brackets (e.g., `<id>`) being hidden in chat prompt display due to HTML parsing. [#929](https://github.com/sourcebot-dev/sourcebot/pull/929)
18+
- Fixed text inside angle brackets (e.g., `<id>`) being hidden in chat prompt display due to HTML parsing. [#929](https://github.com/sourcebot-dev/sourcebot/pull/929) [#932](https://github.com/sourcebot-dev/sourcebot/pull/932)
1919

2020
## [4.11.7] - 2026-02-23
2121

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

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

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

Lines changed: 31 additions & 17 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) => {
@@ -102,32 +123,27 @@ interface MarkdownRendererProps {
102123
content: string;
103124
className?: string;
104125
/**
105-
* When true, disables raw HTML parsing. This prevents text like `<id>` from
106-
* being interpreted as HTML tags. Use this for user-provided content that
107-
* shouldn't contain embedded HTML.
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.
108128
*/
109-
disableRawHtml?: boolean;
129+
escapeHtml?: boolean;
110130
}
111131

112-
const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererProps>(({ content, className, disableRawHtml = false }, ref) => {
132+
const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererProps>(({ content, className, escapeHtml = false }, ref) => {
113133
const router = useRouter();
114134

115135
const remarkPlugins = useMemo((): PluggableList => {
116136
return [
117137
remarkGfm,
138+
...(escapeHtml ? [remarkPreserveHtml] : []),
118139
remarkReferencesPlugin,
119140
remarkTocExtractor,
120141
];
121-
}, []);
142+
}, [escapeHtml]);
122143

123144
const rehypePlugins = useMemo((): PluggableList => {
124-
const plugins: PluggableList = [];
125-
126-
if (!disableRawHtml) {
127-
plugins.push(rehypeRaw);
128-
}
129-
130-
plugins.push(
145+
return [
146+
rehypeRaw,
131147
[
132148
rehypeSanitize,
133149
{
@@ -140,10 +156,8 @@ const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererPro
140156
} satisfies SanitizeSchema,
141157
],
142158
annotateCodeBlocks,
143-
);
144-
145-
return plugins;
146-
}, [disableRawHtml]);
159+
];
160+
}, []);
147161

148162
const renderPre = useCallback(({ children, node, ...rest }: React.JSX.IntrinsicElements['pre'] & { node?: Element }) => {
149163
if (node?.properties && node.properties.isBlock === true) {

0 commit comments

Comments
 (0)