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:
brooklyn! 2026-05-11 14:16:31 -07:00 committed by GitHub
parent 9a63b5f16c
commit 75b428c852
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 644 additions and 16 deletions

View file

@ -218,6 +218,41 @@ describe('Md wrapping', () => {
})
})
describe('Md link labels', () => {
it('renders bare URLs with readable slug labels', () => {
const lines = renderPlain(
React.createElement(
Box,
{ width: 120 },
React.createElement(Md, {
t: DEFAULT_THEME,
text: 'see https://www.expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure for details'
})
)
)
const rendered = lines.join('\n')
expect(rendered).toContain('Puerto Rico El Yunque Rainforest Adventure')
expect(rendered).not.toContain('https://www.expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure')
})
it('keeps explicit markdown labels as the immediate fallback', () => {
const lines = renderPlain(
React.createElement(
Box,
{ width: 80 },
React.createElement(Md, {
t: DEFAULT_THEME,
text: '[Trip details](https://www.expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure)'
})
)
)
expect(lines.join('\n')).toContain('Trip details')
})
})
describe('renderTable CJK width alignment', () => {
it('column starts share the same display offset across CJK rows', async () => {
const { stringWidth } = await import('@hermes/ink')
@ -248,6 +283,7 @@ describe('renderTable CJK width alignment', () => {
// unique anchor for column 2's start position on each row.
const colStarts = (line: string, anchor: string): number => {
const idx = line.indexOf(anchor)
return idx < 0 ? -1 : stringWidth(line.slice(0, idx))
}