Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Hide version upgrade toast for askgithub deployment (`EXPERIMENT_ASK_GH_ENABLED`). [#931](https://github.com/sourcebot-dev/sourcebot/pull/931)

### Fixed
- 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)
- 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)

## [4.11.7] - 2026-02-23

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
<MarkdownRenderer
content={userQuestion.trim()}
className="prose-p:m-0"
disableRawHtml={true}
escapeHtml={true}
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,27 @@ function remarkReferencesPlugin() {
}
}

/**
* A remark plugin that converts `html` MDAST nodes into `text` nodes,
* preserving angle-bracketed content like `<id>` as visible text. Without this,
* `<id>` is parsed as an HTML tag and then stripped by sanitization.
*
* This plugin must run BEFORE remarkReferencesPlugin so that the file-reference
* HTML nodes created by that plugin are left intact for rehypeRaw to process.
*/
function remarkPreserveHtml() {
return function (tree: Nodes) {
visit(tree, 'html', (node, index, parent) => {
if (index !== undefined && parent && 'children' in parent) {
(parent.children as Nodes[])[index] = {
type: 'text',
value: (node as { value: string }).value,
};
}
});
};
}

const remarkTocExtractor = () => {
return function (tree: Nodes) {
visit(tree, 'heading', (node: Heading) => {
Expand All @@ -102,32 +123,27 @@ interface MarkdownRendererProps {
content: string;
className?: string;
/**
* When true, disables raw HTML parsing. This prevents text like `<id>` from
* being interpreted as HTML tags. Use this for user-provided content that
* shouldn't contain embedded HTML.
* When true, angle-bracketed text like `<id>` is preserved as visible text
* instead of being parsed as HTML. File references (@file:{...}) are unaffected.
*/
disableRawHtml?: boolean;
escapeHtml?: boolean;
}

const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererProps>(({ content, className, disableRawHtml = false }, ref) => {
const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererProps>(({ content, className, escapeHtml = false }, ref) => {
const router = useRouter();

const remarkPlugins = useMemo((): PluggableList => {
return [
remarkGfm,
...(escapeHtml ? [remarkPreserveHtml] : []),
remarkReferencesPlugin,
remarkTocExtractor,
];
}, []);
}, [escapeHtml]);

const rehypePlugins = useMemo((): PluggableList => {
const plugins: PluggableList = [];

if (!disableRawHtml) {
plugins.push(rehypeRaw);
}

plugins.push(
return [
rehypeRaw,
[
rehypeSanitize,
{
Expand All @@ -140,10 +156,8 @@ const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererPro
} satisfies SanitizeSchema,
],
annotateCodeBlocks,
);

return plugins;
}, [disableRawHtml]);
];
}, []);

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