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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- Links in Ask Sourcebot chat responses now open in a new tab with a subtle external link icon indicator. [#1059](https://github.com/sourcebot-dev/sourcebot/pull/1059)

## [4.16.5] - 2026-04-02

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SearchQueryParams } from '@/lib/types';
import { cn, createPathWithQueryParams } from '@/lib/utils';
import type { Element, Root } from "hast";
import { Schema as SanitizeSchema } from 'hast-util-sanitize';
import { CopyIcon, SearchIcon } from 'lucide-react';
import { CopyIcon, ExternalLinkIcon, SearchIcon } from 'lucide-react';
import type { Heading, Nodes } from "mdast";
import { findAndReplace } from 'mdast-util-find-and-replace';
import { useRouter } from 'next/navigation';
Expand Down Expand Up @@ -171,6 +171,21 @@ const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererPro
)
}, []);

const renderAnchor = useCallback(({ href, children, ...rest }: React.JSX.IntrinsicElements['a']) => {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-0.5"
{...rest}
>
{children}
<ExternalLinkIcon className="inline w-3 h-3 mb-0.5 opacity-60" />
</a>
);
}, []);
Comment on lines +174 to +187
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.

⚠️ Potential issue | 🟡 Minor

Move {...rest} before explicit props to preserve security attributes.

The spread {...rest} appears after target, rel, and className. If rest contains any of these props (e.g., from the markdown AST), they will override your explicit values. This could inadvertently remove the rel="noopener noreferrer" security attribute.

🛡️ Proposed fix
 const renderAnchor = useCallback(({ href, children, ...rest }: React.JSX.IntrinsicElements['a']) => {
     return (
         <a
+            {...rest}
             href={href}
             target="_blank"
             rel="noopener noreferrer"
-            className="inline-flex items-center gap-0.5"
-            {...rest}
+            className={cn("inline-flex items-center gap-0.5", rest.className)}
         >
             {children}
             <ExternalLinkIcon className="inline w-3 h-3 mb-0.5 opacity-60" />
         </a>
     );
 }, []);

Note: If you want to preserve any incoming className from markdown parsing, use the cn utility (already imported) to merge them. Otherwise, simply placing the spread first is sufficient.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const renderAnchor = useCallback(({ href, children, ...rest }: React.JSX.IntrinsicElements['a']) => {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-0.5"
{...rest}
>
{children}
<ExternalLinkIcon className="inline w-3 h-3 mb-0.5 opacity-60" />
</a>
);
}, []);
const renderAnchor = useCallback(({ href, children, ...rest }: React.JSX.IntrinsicElements['a']) => {
return (
<a
{...rest}
href={href}
target="_blank"
rel="noopener noreferrer"
className={cn("inline-flex items-center gap-0.5", rest.className)}
>
{children}
<ExternalLinkIcon className="inline w-3 h-3 mb-0.5 opacity-60" />
</a>
);
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx`
around lines 174 - 187, The spread {...rest} in the renderAnchor component can
override explicit security props (target, rel, className); move {...rest} before
the explicit props inside the <a> element so incoming props cannot overwrite
target="_blank" and rel="noopener noreferrer". If you need to preserve incoming
className from the markdown AST, merge it with the existing class via the cn
utility (e.g., cn(rest.className, "inline-flex items-center gap-0.5")) and
remove className from rest before spreading to avoid duplication.


const renderCode = useCallback(({ className, children, node, ...rest }: React.JSX.IntrinsicElements['code'] & { node?: Element }) => {
const text = children?.toString().trimEnd() ?? '';

Expand Down Expand Up @@ -239,6 +254,7 @@ const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererPro
components={{
pre: renderPre,
code: renderCode,
a: renderAnchor,
}}
>
{content}
Expand Down
Loading