feat(web): open chat links in new tab with external link icon#1059
feat(web): open chat links in new tab with external link icon#1059
Conversation
- Add custom anchor renderer to MarkdownRenderer component - Set target='_blank' and rel='noopener noreferrer' on all links - Display subtle ExternalLinkIcon (↗) after link text - Icon uses opacity-60 for muted appearance in both themes Fixes SOU-822 Co-authored-by: Michael Sukkarieh <msukkari@users.noreply.github.com>
WalkthroughUpdates the Markdown renderer in the chat component to render links with Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-authored-by: Michael Sukkarieh <msukkari@users.noreply.github.com>
Co-authored-by: Michael Sukkarieh <msukkari@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx`:
- Around line 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.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9e28e8d3-6521-49d9-acd9-e2672506455d
📒 Files selected for processing (2)
CHANGELOG.mdpackages/web/src/features/chat/components/chatThread/markdownRenderer.tsx
| 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> | ||
| ); | ||
| }, []); |
There was a problem hiding this comment.
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.
| 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.
Overview
This PR makes links in Ask Sourcebot chat responses open in new tabs and adds a subtle external link icon (↗) to indicate this behavior.
Changes
Added a custom anchor renderer (
renderAnchor) to theMarkdownRenderercomponent that:target="_blank"andrel="noopener noreferrer"on all links for new-tab behavior and securityExternalLinkIconfrom lucide-react inline after the link textopacity-60,w-3 h-3) so the icon doesn't overwhelm the contentDemo
external_link_demo.mp4
Screenshots
Chat message showing links with external link icons
Close-up of external link icon styling
Testing
Fixes #SOU-822
Linear Issue: SOU-822
Summary by CodeRabbit