// WebBlock: the surface for a completed web retrieval. One component draws both // kinds of the `web` render intent, discriminated by `kind`: a `search` shows an // optional provider answer above a citation list of sources (each a safe // external link labelled by its title, or its hostname when the provider gave // none, with the snippet and publication date below it), and a `fetch` shows a // compact retrieval summary (the linked final URL and its HTTP status). Both // mark a capped retrieval. Every link is a same-origin-safe external anchor: // only http(s) URLs become anchors (target/rel set) — the http(s) subset of the // allowlist MarkdownText applies to untrusted assistant-authored links (it also // permits mailto, excluded here); an unparseable or non-http URL renders as // plain text. Geometry, radius, and fonts mirror CodeBlock/TerminalBlock so a // web card reads as one family with them; a long source list caps at maxSources // with a head/tail collapse using the same arithmetic as TerminalBlock's output // cap. import { useCallback, useState } from 'react' import clsx from 'clsx' import { MarkdownText } from './markdown/MarkdownText.tsx' import css from './WebBlock.module.css' /** * Sources shown before the height cap collapses the middle of a citation list. * Matches TerminalBlock's default output budget so both cards cut a long body * at the same place; the chat row narrows it through the maxSources prop. */ export const DEFAULT_WEB_MAX_SOURCES = 16 /** * One citeable source drawn in a search card: the projection of the contract's * `WebSource`, with the optional fields kept optional so a provider that * returned only a URL still renders (its hostname becomes the label). */ export interface WebSourceView { /** The source URL; becomes a safe external link when it is http(s). */ url: string /** The source title; when absent the URL's hostname labels the link. */ title?: string | undefined /** A short excerpt or summary shown under the link. */ snippet?: string | undefined /** Publication/crawl timestamp, a provider-supplied string shown under the link. */ publishedAt?: string | undefined } /** A `web_search` card: an optional answer over a capped citation list. */ export interface WebSearchBlockProps { kind: 'search' /** The provider-generated answer, rendered as markdown above the sources. */ answer?: string | undefined /** The cited sources, in provider order. */ sources: WebSourceView[] /** True when the tool cut the source list to its result cap. */ truncated: boolean /** Sources shown before the middle collapses (default {@link DEFAULT_WEB_MAX_SOURCES}). */ maxSources?: number | undefined /** Extra class merged onto the wrapper (callers position; this component draws). */ className?: string | undefined } /** A `web_fetch` card: the retrieval summary for one fetched URL. */ export interface WebFetchBlockProps { kind: 'fetch' /** The final URL after allowed redirects; becomes a safe external link when http(s). */ url: string /** HTTP status code of the fetched response. */ statusCode: number /** True when the provider or the output cap cut the fetched content. */ truncated: boolean /** * Accepted and ignored, so both card kinds take one uniform prop set (a fetch * card has no source list to cap) — the same way TerminalBlock accepts one * `maxLines` across its arms. Lets a render site spread `maxSources` onto * either kind without a per-kind conditional. */ maxSources?: number | undefined /** Extra class merged onto the wrapper (callers position; this component draws). */ className?: string | undefined } /** A completed web retrieval card, discriminated by `kind`. */ export type WebBlockProps = WebSearchBlockProps | WebFetchBlockProps /** * The URL to link to, or undefined when the URL must render as plain text. Only * http(s) becomes a navigable external anchor, so a `javascript:`/`data:`/`file:` * URL or an unparseable string never reaches the DOM as an href. This is the * http(s) subset of the allowlist MarkdownText applies to untrusted links — * MarkdownText also permits `mailto:`, deliberately excluded here since a * retrieval URL is never a mail address. * @param url - the source or fetch URL, from tool result content. * @returns the href to use, or undefined for plain text. */ function safeHref(url: string): string | undefined { try { const { protocol } = new URL(url) return protocol === 'http:' || protocol === 'https:' ? url : undefined } catch { return undefined } } /** * The link's visible label: the title when the provider gave one, otherwise the * URL's hostname, falling back to the raw URL when it does not parse OR parses * to an empty hostname (a `file:`/`data:`/`javascript:` URL), so a label is * never blank. * @param url - the source URL. * @param title - the provider title, if any. * @returns the label text. */ function linkLabel(url: string, title: string | undefined): string { if (title !== undefined && title !== '') return title try { const { hostname } = new URL(url) return hostname === '' ? url : hostname } catch { return url } } /** * A single URL rendered as a safe external anchor, or as plain text when the * URL is not an http(s) link. * @param props.url - the URL to render. * @param props.label - the visible label. * @param props.className - class for the anchor or the plain span. * @returns the anchor or span element. */ function SafeLink({ url, label, className }: { url: string; label: string; className?: string | undefined }) { const href = safeHref(url) if (href === undefined) return {label} return ( {label} ) } /** * One source row in a search card: the safe link plus its snippet and date. The * `