feat(frontend/copilot): align web_search UI with native WebSearch rendering

- Map ``web_search`` to the ``web`` tool category so the MCP copilot
  tool shares the globe icon + accordion layout with the SDK's native
  ``WebSearch``.
- Render the structured ``results`` array (title / url / snippet /
  page_age) as clickable citation list instead of dumping JSON.  Falls
  back to the existing ``content`` / MCP text / raw JSON path for the
  pre-existing ``web_fetch`` + native ``WebSearch`` shapes.
This commit is contained in:
majdyz
2026-04-22 00:11:46 +07:00
parent 799201bbe9
commit e7457983a1
2 changed files with 52 additions and 10 deletions

View File

@@ -305,15 +305,60 @@ function getWebAccordionData(
string,
unknown
>;
const url =
getStringField(inp as Record<string, unknown>, "url", "query") ??
"Web content";
const query = getStringField(inp, "query");
const url = getStringField(inp, "url") ?? query ?? "Web content";
const results = Array.isArray(output.results)
? (output.results as Array<Record<string, unknown>>)
: null;
if (results) {
return {
title: `${results.length} search result${results.length === 1 ? "" : "s"}`,
description: query ? truncate(query, 80) : undefined,
content: (
<div className="space-y-3">
{results.map((r, i) => {
const title = getStringField(r, "title") ?? "(untitled)";
const href = getStringField(r, "url") ?? "";
const snippet = getStringField(r, "snippet");
const pageAge = getStringField(r, "page_age");
return (
<div key={i} className="text-sm">
{href ? (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-blue-600 hover:underline"
>
{title}
</a>
) : (
<span className="font-medium">{title}</span>
)}
{href && (
<div className="text-xs text-slate-500">
{truncate(href, 100)}
</div>
)}
{snippet && (
<p className="mt-0.5 text-slate-700">{snippet}</p>
)}
{pageAge && (
<div className="mt-0.5 text-xs text-slate-400">{pageAge}</div>
)}
</div>
);
})}
</div>
),
};
}
// Try direct string fields first, then MCP content blocks, then raw JSON
let content = getStringField(output, "content", "text", "_raw");
if (!content) content = extractMcpText(output);
if (!content) {
// Fallback: render the raw JSON so the accordion isn't empty
try {
const raw = JSON.stringify(output, null, 2);
if (raw !== "{}") content = raw;
@@ -327,11 +372,7 @@ function getWebAccordionData(
const message = getStringField(output, "message");
return {
title: statusCode
? `Response (${statusCode})`
: url
? "Web fetch"
: "Search results",
title: statusCode ? `Response (${statusCode})` : "Web fetch",
description: truncate(url, 80),
content: content ? (
<ContentCodeBlock>{content}</ContentCodeBlock>

View File

@@ -60,6 +60,7 @@ export function getToolCategory(toolName: string): ToolCategory {
case "bash_exec":
return "bash";
case "web_fetch":
case "web_search":
case "WebSearch":
case "WebFetch":
return "web";