mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix(desktop): render debug-report paste URLs as real clickable links
System messages (slash-command output like /debug, plus the generic system-message fallback) were rendered as plain text, so the uploaded paste.rs URLs in a debug report were neither clickable nor easily copyable. Route both through LinkifiedText so URLs become real <a> links (open externally via the desktop bridge, selectable/copyable text). Add an opt-in explicitOnly mode that matches only explicit http(s):// / www. URLs, used here so filename-shaped tokens in the report (agent.log, errors.log, gateway.log) aren't mistaken for bare domains and linkified. Bare-domain matching is preserved for all other LinkifiedText callers. Adds regression tests covering explicitOnly (links only real URLs, keeps .log filenames as text) and the default bare-domain behavior.
This commit is contained in:
parent
b5f8996ccc
commit
e0f6a35ac6
3 changed files with 39 additions and 4 deletions
|
|
@ -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 = () => {
|
|||
>
|
||||
<span className="font-mono text-muted-foreground/55">{slashStatus.groups.command}</span>
|
||||
<span className="mx-1.5 text-muted-foreground/35">·</span>
|
||||
<span className="whitespace-pre-wrap">{slashStatus.groups.output.trim()}</span>
|
||||
<LinkifiedText className="whitespace-pre-wrap" explicitOnly pretty={false} text={slashStatus.groups.output.trim()} />
|
||||
</MessagePrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
|
@ -930,7 +931,7 @@ const SystemMessage: FC = () => {
|
|||
data-role="system"
|
||||
data-slot="aui_system-message-root"
|
||||
>
|
||||
<span className="whitespace-pre-wrap">{text}</span>
|
||||
<LinkifiedText className="whitespace-pre-wrap" explicitOnly pretty={false} text={text} />
|
||||
</MessagePrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<LinkifiedText
|
||||
explicitOnly
|
||||
pretty={false}
|
||||
text={'Report https://paste.rs/abc\nagent.log https://paste.rs/def\nerrors.log'}
|
||||
/>
|
||||
)
|
||||
|
||||
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(<LinkifiedText pretty={false} text="open agent.log please" />)
|
||||
|
||||
const link = screen.getByRole('link', { name: 'agent.log' })
|
||||
expect(link.getAttribute('href')).toBe('https://agent.log')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ const titleSubs = new Map<string, Set<(value: string) => 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue