Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Bounded the ESC-cancel worker-thread queue in `OpenAICompatibleProvider.chat_stream_response` (`src/providers/openai_compatible.py`) to `maxsize=64`. Previously an unbounded `queue.Queue` let an orphaned worker accumulate chunks in memory indefinitely when a proxy kept sending bytes after abort without closing the SDK iterator (#278).
- **URLs the agent prints are clickable again.** The TUI markdown renderer
(`ui-tui/src/components/markdown.tsx`) replaced every visible URL with a
remote-fetched page `<title>` (silently HTTP-GETting each URL the agent
printed) or a slug-derived label, leaving the real URL only in OSC 8
metadata. Terminals without OSC 8 support (e.g. Apple Terminal) strip
that metadata, so the URL was invisible, unclickable, and uncopyable —
and in the default inline mode the TUI never captures the mouse, so
terminal-native detection over the visible text is the only affordance
that works everywhere. Bare URLs now render verbatim, `[label](url)`
renders as `label (url)`, the stealth title fetch is gone, and links
remain OSC 8-wrapped for terminals with first-class hyperlink support
(Cmd+click in VS Code/iTerm2, plain click in fullscreen mode).

### Changed

Expand Down
144 changes: 6 additions & 138 deletions ui-tui/src/__tests__/externalLink.test.ts
Original file line number Diff line number Diff line change
@@ -1,149 +1,17 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { describe, expect, it } from 'vitest'

import {
__resetLinkTitleCache,
fetchLinkTitle,
hostPathLabel,
isTitleFetchable,
normalizeExternalUrl,
urlSlugTitleLabel
} from '../lib/externalLink.js'

afterEach(() => {
__resetLinkTitleCache()
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
import { normalizeExternalUrl } from '../lib/externalLink.js'

describe('external link helpers', () => {
it('formats URL fallbacks as host + path', () => {
expect(
hostPathLabel(
'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
)
).toBe('getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894')
})

it('derives readable title fallbacks from URL slugs', () => {
expect(
urlSlugTitleLabel(
'https://www.getyourguide.com/fajardo-l882/from-fajardo-icacos-island-full-day-catamaran-trip-t19891/'
)
).toBe('From Fajardo Icacos Island Full Day Catamaran Trip')
})

it('keeps x.com status fallbacks link-like instead of generic Status labels', () => {
expect(urlSlugTitleLabel('https://x.com/grok/status/2056065022749479209')).toBe(
'x.com/grok/status/2056065022749479209'
)
})

it('normalizes scheme-less links', () => {
expect(normalizeExternalUrl(' expedia.com/things-to-do/puerto-rico-el-yunque ')).toBe(
'https://expedia.com/things-to-do/puerto-rico-el-yunque'
)
})

it('filters out local/non-http targets for title fetches', () => {
expect(isTitleFetchable('https://www.expedia.com/things-to-do/foo')).toBe(true)
expect(isTitleFetchable('http://localhost:5174')).toBe(false)
expect(isTitleFetchable('file:///tmp/demo.html')).toBe(false)
expect(isTitleFetchable('mailto:hello@example.com')).toBe(false)
})

it('blocks private, link-local, and intranet hosts', () => {
expect(isTitleFetchable('http://10.0.0.12/path')).toBe(false)
expect(isTitleFetchable('http://172.22.5.4/path')).toBe(false)
expect(isTitleFetchable('http://192.168.1.22/path')).toBe(false)
expect(isTitleFetchable('http://169.254.169.254/latest/meta-data')).toBe(false)
expect(isTitleFetchable('http://[fd00::1]/')).toBe(false)
expect(isTitleFetchable('http://[fe80::1]/')).toBe(false)
expect(isTitleFetchable('http://printer.local/status')).toBe(false)
expect(isTitleFetchable('http://intranet/status')).toBe(false)
expect(isTitleFetchable('https://8.8.8.8/status')).toBe(true)
})

it('deduplicates in-flight title fetches and caches results', async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response('<html><head><title>El Yunque Tour Water Slide, Rope Swing & Pickup</title></head></html>', {
headers: { 'content-type': 'text/html; charset=utf-8' },
status: 200
})
)

vi.stubGlobal('fetch', fetchMock)

const url =
'https://www.expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure.a46272756.activity-details'

const [first, second] = await Promise.all([fetchLinkTitle(url), fetchLinkTitle(url)])

expect(first).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
expect(second).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
expect(fetchMock).toHaveBeenCalledTimes(1)

const third = await fetchLinkTitle(url)

expect(third).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
expect(fetchMock).toHaveBeenCalledTimes(1)
})

it('shares cache across protocol/www URL variants', async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response('<html><head><title>Shared Canonical Title</title></head></html>', {
headers: { 'content-type': 'text/html' },
status: 200
})
)

vi.stubGlobal('fetch', fetchMock)

const first = 'https://www.getyourguide.com/san-juan-puerto-rico-l355/sunset-tours-tc306/'
const second = 'http://getyourguide.com/san-juan-puerto-rico-l355/sunset-tours-tc306/'

const [a, b] = await Promise.all([fetchLinkTitle(first), fetchLinkTitle(second)])

expect(a).toBe('Shared Canonical Title')
expect(b).toBe('Shared Canonical Title')
expect(fetchMock).toHaveBeenCalledTimes(1)
})

it('ignores error-like fetched titles', async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response('<html><head><title>Just a moment...</title></head></html>', {
headers: { 'content-type': 'text/html' },
status: 200
})
)

vi.stubGlobal('fetch', fetchMock)

const url =
'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'

await expect(fetchLinkTitle(url)).resolves.toBe('')
})

it('decodes HTML entities in fetched titles', async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response('<html><head><title>AT&amp;T &#39;Deals&#39;</title></head></html>', {
headers: { 'content-type': 'text/html' },
status: 200
})
)

vi.stubGlobal('fetch', fetchMock)

await expect(fetchLinkTitle('https://example.com/offers')).resolves.toBe("AT&T 'Deals'")
})

it('skips network fetch for non-fetchable targets', async () => {
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)

await expect(fetchLinkTitle('http://localhost:3000/path')).resolves.toBe('')
await expect(fetchLinkTitle('mailto:hello@example.com')).resolves.toBe('')
await expect(fetchLinkTitle('file:///tmp/demo.html')).resolves.toBe('')
expect(fetchMock).not.toHaveBeenCalled()
it('leaves URLs with schemes and non-domain text untouched', () => {
expect(normalizeExternalUrl('https://example.com/docs')).toBe('https://example.com/docs')
expect(normalizeExternalUrl('mailto:user@example.com')).toBe('mailto:user@example.com')
expect(normalizeExternalUrl('not a url')).toBe('not a url')
})
})
Loading
Loading