import type { JSX } from "react";
import { translate } from "../../../../i18n/index";
import {
arrayValue,
dedupeToolSectionContent,
optionRecord,
stringValue,
ToolMarkdownBlock,
ToolSection,
type AgentToolRendererProps
} from "./agentToolContentShared";
import { getWebSearchRenderData } from "./render-data/agentToolRenderData";
const MAX_SUMMARY_LENGTH = 3000;
export function AgentWebSearchContent({
call,
onLinkClick
}: AgentToolRendererProps): JSX.Element | null {
"use memo";
const web = getWebSearchRenderData(call);
const queries = web.queries;
const outputText = web.output;
const links = normalizeLinks(call.output?.links, outputText);
const queryText = webSearchQueryText(web.query, queries);
const summary = extractSummary(outputText);
const visibleSummary = dedupeToolSectionContent(
summary ? summary.slice(0, MAX_SUMMARY_LENGTH) : null,
queryText,
links.map((link) => `${link.domain} ${link.title}`).join("\n")
);
const hasRenderableContent = Boolean(
queryText || links.length > 0 || visibleSummary || web.error
);
if (!hasRenderableContent) {
return null;
}
return (
{queryText ? (
) : null}
{links.length > 0 ? (
) : null}
{visibleSummary ? (
) : null}
{summary && summary.length > MAX_SUMMARY_LENGTH ? (
{translate("agentHost.agentTool.details.summaryTruncated")}
) : null}
{web.error ? (
) : null}
);
}
function webSearchQueryText(
query: string | null,
queries: readonly string[]
): string | null {
const candidates = queries.length > 0 ? queries : query ? [query] : [];
const deduped = [
...new Set(candidates.map((value) => value.trim()).filter(Boolean))
];
return deduped.length > 0 ? deduped.join("\n") : null;
}
function normalizeLinks(
value: unknown,
output: string
): Array<{ title: string; url: string; domain: string }> {
const explicitLinks =
arrayValue(value)
?.map(optionRecord)
.filter(
(candidate): candidate is Record => candidate !== null
)
.flatMap((link) => {
const url = stringValue(link.url);
if (!url) {
return [];
}
return [
{
title: stringValue(link.title) ?? url,
url,
domain: domainForUrl(url)
}
];
}) ?? [];
if (explicitLinks.length > 0) {
return explicitLinks;
}
const match = output.match(/^Links:\s*(\[[\s\S]*?\])(?:\n\n|\n|$)/);
if (!match) {
return extractQuotedLinks(output);
}
try {
const linksJson = match[1];
if (!linksJson) {
return extractQuotedLinks(output);
}
const parsed = JSON.parse(linksJson) as unknown[];
return parsed.flatMap((entry) => {
const link = optionRecord(entry);
const url = stringValue(link?.url);
if (!url) {
return [];
}
return [
{
title: stringValue(link?.title) ?? url,
url,
domain: domainForUrl(url)
}
];
});
} catch {
return extractQuotedLinks(output);
}
}
function extractQuotedLinks(
output: string
): Array<{ title: string; url: string; domain: string }> {
return Array.from(
output.matchAll(/"title":"([^"]+)"\s*,\s*"url":"([^"]+)"/g)
).flatMap((entry) => {
const title = entry[1];
const url = entry[2];
if (!title || !url) {
return [];
}
return [{ title, url, domain: domainForUrl(url) }];
});
}
function extractSummary(output: string): string | null {
const trimmed = output.trim();
if (!trimmed) {
return null;
}
const withLinksRemoved = trimmed
.replace(/^Links:\s*\[[\s\S]*?\](?:\n\n|\n)?/, "")
.trim();
return withLinksRemoved || (!trimmed.startsWith("Links:") ? trimmed : null);
}
function domainForUrl(url: string): string {
try {
return new URL(url).hostname.replace(/^www\./, "");
} catch {
return url;
}
}