diff --git a/apps/desktop/src/components/assistant-ui/thread.tsx b/apps/desktop/src/components/assistant-ui/thread.tsx index 32501069fa7..ca7533c4c04 100644 --- a/apps/desktop/src/components/assistant-ui/thread.tsx +++ b/apps/desktop/src/components/assistant-ui/thread.tsx @@ -77,6 +77,7 @@ import type { HermesGateway } from '@/hermes' import { useResizeObserver } from '@/hooks/use-resize-observer' import { useI18n } from '@/i18n' import { DATA_IMAGE_URL_RE } from '@/lib/embedded-images' +import { LinkifiedText } from '@/lib/external-link' import { triggerHaptic } from '@/lib/haptics' import { GitBranchIcon, Loader2Icon, Volume2Icon, VolumeXIcon } from '@/lib/icons' import { extractPreviewTargets } from '@/lib/preview-targets' @@ -919,7 +920,7 @@ const SystemMessage: FC = () => { > {slashStatus.groups.command} · - {slashStatus.groups.output.trim()} + ) } @@ -930,7 +931,7 @@ const SystemMessage: FC = () => { data-role="system" data-slot="aui_system-message-root" > - {text} + ) } diff --git a/apps/desktop/src/lib/external-link.test.tsx b/apps/desktop/src/lib/external-link.test.tsx index 4e529576548..5001f9c479a 100644 --- a/apps/desktop/src/lib/external-link.test.tsx +++ b/apps/desktop/src/lib/external-link.test.tsx @@ -165,4 +165,31 @@ describe('external link helpers', () => { 'https://expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure' ) }) + + it('explicitOnly skips bare filename/domain tokens and only links explicit URLs', () => { + installDesktopBridge() + + render( + + ) + + const links = screen.getAllByRole('link') + expect(links.map(a => a.getAttribute('href'))).toEqual(['https://paste.rs/abc', 'https://paste.rs/def']) + // Bare filename-shaped tokens stay as plain text, not links. + expect(screen.queryByText(content => content.includes('agent.log'))).toBeTruthy() + expect(links.some(a => (a.textContent ?? '').includes('.log'))).toBe(false) + }) + + it('without explicitOnly, bare filename tokens are still linkified (default behavior)', () => { + installDesktopBridge() + + render() + + const link = screen.getByRole('link', { name: 'agent.log' }) + expect(link.getAttribute('href')).toBe('https://agent.log') + }) }) diff --git a/apps/desktop/src/lib/external-link.tsx b/apps/desktop/src/lib/external-link.tsx index 05f1ec02f66..ebdee577ac2 100644 --- a/apps/desktop/src/lib/external-link.tsx +++ b/apps/desktop/src/lib/external-link.tsx @@ -12,6 +12,12 @@ const titleSubs = new Map void>>() const URL_RE = /(?:https?:\/\/|www\.)[^\s<>"'`]+[^\s<>"'`.,;:!?)]|[a-z0-9](?:[a-z0-9-]*\.)+[a-z]{2,}(?:\/[^\s<>"'`.,;:!?)]*)?/gi +// Explicit-scheme / www. URLs only — no bare-domain matching. Used where the +// surrounding text is full of filename-shaped tokens (e.g. `agent.log`, +// `errors.log` in a /debug report) that the bare-domain branch of URL_RE would +// otherwise mistake for domains and linkify. +const EXPLICIT_URL_RE = /(?:https?:\/\/|www\.)[^\s<>"'`]+[^\s<>"'`.,;:!?)]/gi + const DOMAIN_RE = /^(?:www\.)?[a-z0-9](?:[a-z0-9-]*\.)+[a-z]{2,}(?::\d+)?(?:[/?#][^\s]*)?$/i const SKIP_PROTO_RE = /^(?:file|data|mailto|javascript|blob|chrome|about|hermes):/i const LOCAL_HOST_RE = /^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?$/i @@ -261,13 +267,14 @@ interface LinkifiedTextProps { className?: string text: string pretty?: boolean + explicitOnly?: boolean } -export function LinkifiedText({ className, pretty = true, text }: LinkifiedTextProps) { +export function LinkifiedText({ className, explicitOnly = false, pretty = true, text }: LinkifiedTextProps) { const nodes: ReactNode[] = [] let cursor = 0 - for (const match of text.matchAll(URL_RE)) { + for (const match of text.matchAll(explicitOnly ? EXPLICIT_URL_RE : URL_RE)) { const raw = match[0] const url = normalizeExternalUrl(raw) const index = match.index ?? 0