feat(xai): surface hosted X Search activity (#5050)

This commit is contained in:
chengyongru
2026-07-23 13:42:09 +08:00
committed by GitHub
parent f3099286ea
commit 9cf2fb19c2
14 changed files with 431 additions and 8 deletions
@@ -10,7 +10,7 @@ import {
export function WebSearchRun({ run, turnActive }: { run: WebSearchRunModel; turnActive: boolean }) {
const active = run.status === "running" && turnActive;
const status = run.status === "running" && !turnActive ? "done" : run.status;
const label = presentWebSearchAction(run.query, status);
const label = presentWebSearchAction(run.query, status, run.target);
return (
<>
@@ -77,6 +77,7 @@ const EXCLUDED_TOOLS = new Set([
"terminal",
"web_fetch",
"web_search",
"x_search",
"write_file",
]);
@@ -30,7 +30,7 @@ export function describeTraceLine(
const query = traceFieldFromArgs(args, ["query", "q", "text"]) || args || trimmed;
return {
kind: "search",
label: presentWebSearchAction(query, status),
label: presentWebSearchAction(query, status, name === "x_search" ? "x" : "web"),
detail: "",
};
}
@@ -5,6 +5,7 @@ import { redactActivityText, safeActivityDetail } from "./activity-text";
import { displayWebHost, formatCompactWebUrl, parseSafeActivityHttpUrl } from "./web-url";
export type WebSearchStatus = "running" | "done" | "error";
export type WebSearchTarget = "web" | "x";
export interface WebSearchSource {
title: string;
@@ -16,6 +17,7 @@ export interface WebSearchSource {
export interface WebSearchRunModel {
key: string;
query: string;
target: WebSearchTarget;
status: WebSearchStatus;
sources: WebSearchSource[];
error?: string;
@@ -49,10 +51,11 @@ export function webSearchRunsByTraceLine(
function webSearchRunFromEvent(event: ToolProgressEvent): WebSearchRunModel | null {
const name = compactToolName(toolEventName(event));
if (name !== "web_search") return null;
if (name !== "web_search" && name !== "x_search") return null;
const args = toolEventArguments(event);
const query = stringField(args, ["query", "q", "text"]);
const target: WebSearchTarget = name === "x_search" ? "x" : "web";
const status: WebSearchStatus = event.phase === "error"
? "error"
: event.phase === "end"
@@ -60,10 +63,11 @@ function webSearchRunFromEvent(event: ToolProgressEvent): WebSearchRunModel | nu
: "running";
return {
key: event.call_id ? `call:${event.call_id}` : formatToolCallTrace(event) ?? `web_search:${query}`,
key: event.call_id ? `call:${event.call_id}` : formatToolCallTrace(event) ?? `${name}:${query}`,
query,
target,
status,
sources: status === "done" ? webSearchSources(event.result) : [],
sources: status === "done" && target === "web" ? webSearchSources(event.result) : [],
error: status === "error" ? readableError(event.error) : undefined,
};
}
@@ -89,6 +93,7 @@ function presentWebSearchQuery(query: string): WebSearchQueryPresentation {
export function presentWebSearchAction(
query: string,
status: WebSearchStatus,
target: WebSearchTarget = "web",
): string {
const presentation = presentWebSearchQuery(query);
const verb = status === "error"
@@ -96,8 +101,11 @@ export function presentWebSearchAction(
: status === "running"
? "Searching"
: "Searched";
const target = [presentation.scope, presentation.query].filter(Boolean).join(" · ");
return target ? `${verb} ${target}` : verb;
const queryTarget = [presentation.scope, presentation.query].filter(Boolean).join(" · ");
if (target === "x") {
return queryTarget ? `${verb} X · ${queryTarget}` : `${verb} X`;
}
return queryTarget ? `${verb} ${queryTarget}` : verb;
}
function mergeWebSearchRun(
@@ -961,6 +961,35 @@ describe("AgentActivityCluster", () => {
expect(screen.getAllByTestId("activity-step")).toHaveLength(3);
});
it("renders hosted X search as an explicit search activity", () => {
const line = 'x_search({"query":"nanobot oauth"})';
render(
<AgentActivityCluster
messages={[{
id: "t-x-search",
role: "tool",
kind: "trace",
content: line,
traces: [line],
toolEvents: [{
phase: "end",
call_id: "x-search-1",
name: "x_search",
arguments: { query: "nanobot oauth" },
result: { name: "x_semantic_search" },
}],
createdAt: 1,
}]}
isTurnStreaming={false}
hasBodyBelow={false}
/>,
);
expect(screen.getByText("Searched X · nanobot oauth")).toBeInTheDocument();
expect(screen.queryByText(/Completed X search/i)).not.toBeInTheDocument();
expect(screen.getAllByTestId("activity-step")).toHaveLength(1);
});
it("redacts credentials from web search queries, titles, and links", () => {
const query = "release notes access_token=signed-secret";
const line = `web_search(${JSON.stringify({ query })})`;
@@ -32,6 +32,14 @@ describe("trace activity semantics", () => {
expect(describeTrace('web_search({"query":"status test"})', status).label).toBe(label);
});
it.each([
["running", "Searching X · status test"],
["done", "Searched X · status test"],
["error", "Could not search X · status test"],
] as const)("identifies hosted X search activity for %s", (status, label) => {
expect(describeTrace('x_search({"query":"status test"})', status).label).toBe(label);
});
it("never exposes URL credentials, query secrets, or private-network links", () => {
const publicResult = describeTrace(
'web_fetch({"url":"https://user:password@example.com/docs?api_key=secret#section"})',