mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-24 05:41:40 +00:00
feat(ui-tui): resolve markdown links to readable page titles (#24013)
* feat(ui-tui): resolve links to readable page titles Mirror desktop pretty-link behavior in the TUI by resolving HTTP links to page titles with shared caching and safe fetch filters, plus slug-based fallbacks so chat links stay readable even when title fetch fails. * refactor(ui-tui): tighten link-title fallback handling Clean up the link-title resolver by hardening in-flight cleanup and clarifying title length limits, while adding focused coverage for HTML entity decoding and markdown-label fallback behavior. * fix(ui-tui): block private-network targets in title fetches Prevent automatic link-title resolution from requesting local or private hosts by rejecting RFC1918, link-local, ULA, and intranet-style hostnames before fetch, and add regression coverage for blocked host patterns.
This commit is contained in:
parent
9a63b5f16c
commit
75b428c852
4 changed files with 644 additions and 16 deletions
|
|
@ -2,6 +2,7 @@ import { Box, Link, stringWidth, Text } from '@hermes/ink'
|
|||
import { Fragment, memo, type ReactNode, useMemo } from 'react'
|
||||
|
||||
import { ensureEmojiPresentation } from '../lib/emoji.js'
|
||||
import { normalizeExternalUrl, urlSlugTitleLabel, useLinkTitle } from '../lib/externalLink.js'
|
||||
import { BOX_CLOSE, BOX_OPEN, texToUnicode } from '../lib/mathUnicode.js'
|
||||
import { highlightLine, isHighlightable } from '../lib/syntax.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
|
@ -143,13 +144,43 @@ const isTableDivider = (row: string) => {
|
|||
const autolinkUrl = (raw: string) =>
|
||||
raw.startsWith('mailto:') || raw.startsWith('http') || !raw.includes('@') ? raw : `mailto:${raw}`
|
||||
|
||||
const renderAutolink = (k: number, t: Theme, raw: string) => (
|
||||
<Link key={k} url={autolinkUrl(raw)}>
|
||||
<Text color={t.color.accent} underline>
|
||||
{raw.replace(/^mailto:/, '')}
|
||||
</Text>
|
||||
</Link>
|
||||
)
|
||||
const defaultLinkLabel = (url: string) =>
|
||||
url.startsWith('mailto:') ? url.replace(/^mailto:/, '') : /^https?:\/\//i.test(url) ? urlSlugTitleLabel(url) : url
|
||||
|
||||
const pickFallbackLabel = (label: string | undefined, target: string): string | undefined => {
|
||||
const trimmed = label?.trim()
|
||||
|
||||
if (!trimmed) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return normalizeExternalUrl(trimmed) === target ? undefined : trimmed
|
||||
}
|
||||
|
||||
interface ResolvedLinkProps {
|
||||
fallbackLabel?: string
|
||||
t: Theme
|
||||
url: string
|
||||
}
|
||||
|
||||
function ResolvedLink({ fallbackLabel, t, url }: ResolvedLinkProps) {
|
||||
const fetched = useLinkTitle(url)
|
||||
const display = fetched || fallbackLabel || defaultLinkLabel(url)
|
||||
|
||||
return (
|
||||
<Link url={url}>
|
||||
<Text color={t.color.accent} underline>
|
||||
{display}
|
||||
</Text>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const renderResolvedLink = (k: number, t: Theme, rawUrl: string, label?: string) => {
|
||||
const target = normalizeExternalUrl(rawUrl)
|
||||
|
||||
return <ResolvedLink fallbackLabel={pickFallbackLabel(label, target)} key={k} t={t} url={target} />
|
||||
}
|
||||
|
||||
export const stripInlineMarkup = (v: string) =>
|
||||
v
|
||||
|
|
@ -232,15 +263,9 @@ function MdInline({ t, text }: { t: Theme; text: string }) {
|
|||
</Text>
|
||||
)
|
||||
} else if (m[3] && m[4]) {
|
||||
parts.push(
|
||||
<Link key={parts.length} url={m[4]}>
|
||||
<Text color={t.color.accent} underline>
|
||||
{m[3]}
|
||||
</Text>
|
||||
</Link>
|
||||
)
|
||||
parts.push(renderResolvedLink(parts.length, t, m[4], m[3]))
|
||||
} else if (m[5]) {
|
||||
parts.push(renderAutolink(parts.length, t, m[5]))
|
||||
parts.push(renderResolvedLink(parts.length, t, autolinkUrl(m[5]), m[5].replace(/^mailto:/, '')))
|
||||
} else if (m[6]) {
|
||||
parts.push(
|
||||
<Text key={parts.length} strikethrough>
|
||||
|
|
@ -302,7 +327,7 @@ function MdInline({ t, text }: { t: Theme; text: string }) {
|
|||
// so `see https://x.com/, which…` keeps the comma outside the link.
|
||||
const url = m[16].replace(/[),.;:!?]+$/g, '')
|
||||
|
||||
parts.push(renderAutolink(parts.length, t, url))
|
||||
parts.push(renderResolvedLink(parts.length, t, url))
|
||||
|
||||
if (url.length < m[16].length) {
|
||||
parts.push(<Text key={parts.length}>{m[16].slice(url.length)}</Text>)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue