diff --git a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx
index 4cd70dbb..e19bf95b 100644
--- a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx
+++ b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx
@@ -17,7 +17,7 @@ import { Icon } from "@/components/icon/Icon";
import { copyTextToClipboard } from '@/lib/clipboard';
import { useI18n } from '@/lib/i18n';
-import { isExternalHttpUrl, isLoopbackHttpUrl, openExternalUrl } from '@/lib/url';
+import { getExternalFaviconUrl, isExternalHttpUrl, isLoopbackHttpUrl, openExternalUrl } from '@/lib/url';
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
import { getDefaultTheme } from '@/lib/theme/themes';
import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator';
@@ -92,6 +92,27 @@ const useExternalLinkInteractions = ({
}, [containerRef, enabled]);
};
+const ExternalLinkFavicon: React.FC<{ href: string }> = ({ href }) => {
+ const [failed, setFailed] = React.useState(false);
+ const faviconUrl = React.useMemo(() => getExternalFaviconUrl(href), [href]);
+
+ if (!faviconUrl || failed) {
+ return null;
+ }
+
+ return (
+
setFailed(true)}
+ />
+ );
+};
+
// Table utility functions
const extractTableData = (tableEl: HTMLTableElement): { headers: string[]; rows: string[][] } => {
const headers: string[] = [];
@@ -935,15 +956,17 @@ const buildMarkdownComponents = ({
},
a({ href, children, ...props }) {
const targetHref = href ?? '';
+ const isExternal = isExternalHttpUrl(targetHref);
const isLoopback = onPreviewLoopback ? isLoopbackHttpUrl(targetHref) : false;
return (
<>
+ {isExternal ? : null}
{children}
{isLoopback && onPreviewLoopback ? (
diff --git a/packages/ui/src/components/chat/message/parts/ProgressiveGroup.tsx b/packages/ui/src/components/chat/message/parts/ProgressiveGroup.tsx
index 0f17bc99..78e0e1b6 100644
--- a/packages/ui/src/components/chat/message/parts/ProgressiveGroup.tsx
+++ b/packages/ui/src/components/chat/message/parts/ProgressiveGroup.tsx
@@ -22,6 +22,7 @@ import { useSkillsStore } from '@/stores/useSkillsStore';
import ReasoningPart from './ReasoningPart';
import JustificationBlock from './JustificationBlock';
import { areRenderRelevantPartsEqual } from '../renderCompare';
+import { getExternalFaviconUrl } from '@/lib/url';
interface ProgressiveGroupProps {
parts: TurnActivityPart[];
@@ -41,6 +42,27 @@ interface ProgressiveGroupProps {
renderJustificationActions?: (activity: TurnActivityPart) => React.ReactNode;
}
+const ExternalLinkFavicon: React.FC<{ href: string }> = ({ href }) => {
+ const [failed, setFailed] = React.useState(false);
+ const faviconUrl = React.useMemo(() => getExternalFaviconUrl(href), [href]);
+
+ if (!faviconUrl || failed) {
+ return null;
+ }
+
+ return (
+
setFailed(true)}
+ />
+ );
+};
+
const isActivityRunning = (activity: TurnActivityPart): boolean => {
if (activity.kind !== 'tool') return false;
const part = activity.part as ToolPartType;
@@ -728,13 +750,14 @@ const StaticToolRowInner: React.FC<{
target="_blank"
rel="noopener noreferrer"
className={cn(
- 'min-w-0 flex-1 underline decoration-[color:var(--status-info)] underline-offset-2 hover:opacity-90',
+ 'min-w-0 flex-1 inline-flex items-center gap-1.5 underline decoration-[color:var(--status-info)] underline-offset-2 hover:opacity-90',
'truncate whitespace-nowrap typography-meta'
)}
style={{ color: 'var(--status-info)' }}
title={url}
>
- {url}
+
+ {url}
))
: null}
diff --git a/packages/ui/src/lib/url.ts b/packages/ui/src/lib/url.ts
index 7b623f8e..e052968f 100644
--- a/packages/ui/src/lib/url.ts
+++ b/packages/ui/src/lib/url.ts
@@ -28,6 +28,15 @@ export const isExternalHttpUrl = (url: string): boolean => {
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
};
+export const getExternalFaviconUrl = (url: string): string | null => {
+ const parsed = parseUrlSafely(url.trim());
+ if (!parsed || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {
+ return null;
+ }
+
+ return `https://icons.duckduckgo.com/ip3/${parsed.hostname.toLowerCase()}.ico`;
+};
+
const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1']);
/**