Files
orca/src/shared/link-preview.ts
T
Kelvin Amoaba 7c5314196a feat(native-chat): preview cards for web links, fetched with a private-address guard (#35221)
* feat(native-chat): preview cards for web links in chat

Hovering an http(s) link that isn't the repo's own PR or issue shows the
page's image, favicon, site name, title and description. The chat's host
fetches the page with no cookies, refusing private, loopback and
link-local addresses after DNS and on every redirect, and returns images
as data URLs. A Chat > Behavior setting turns web previews off.

* perf(native-chat): cap link preview caches at 20 MB each

Preview images are held as data URLs, so a count limit alone let the
renderer and main caches grow to hundreds of MB in a long session.

* fix(link-preview): keep a > inside quoted attribute values

Page tags ended at the first >, so content="Home > Docs" was cut short
and an og tag whose content came before its property was dropped.

* fix(native-chat): keep link preview caches within budget and keyed by host

A preview that finished after newer ones stopped eviction at itself, so
both caches could stay over their byte limit. The GitHub card's load key
also left out the host its repo lives on.

* fix(link-preview): decode the named entities page titles use

Only five named entities were decoded, so titles like
Docs – Guide showed the raw codes in preview cards.
2026-10-29 16:12:07 +00:00

29 lines
1.0 KiB
TypeScript

/** What a page says about itself in its <head>, for a chat link's hover card. */
export type LinkPreview = {
/** The page that answered, after redirects. */
url: string
title: string | null
description: string | null
siteName: string | null
/** Fetched by the host through the same address guard; never a URL the renderer loads. */
imageDataUrl: string | null
faviconDataUrl: string | null
}
export type LinkPreviewRequest = { url: string }
/** `unavailable` covers blocked addresses, non-HTML responses, timeouts and pages with no title. */
export type LinkPreviewResult = { kind: 'page'; preview: LinkPreview } | { kind: 'unavailable' }
/** Characters held by a preview; data URLs are ASCII, so this tracks its memory closely. */
export function linkPreviewSize(preview: LinkPreview): number {
return [
preview.url,
preview.title,
preview.description,
preview.siteName,
preview.imageDataUrl,
preview.faviconDataUrl
].reduce((total, text) => total + (text?.length ?? 0), 0)
}