From 3c6ad5ea3d35db9beb0678bf8cc8a512547e3aa9 Mon Sep 17 00:00:00 2001 From: Alexi Christakis <167903946+alexi-openai@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:20:41 -0700 Subject: [PATCH] Add client-only web search result metadata --- .../analytics/src/analytics_client_tests.rs | 1 + codex-rs/analytics/src/reducer.rs | 4 +- .../schema/json/ServerNotification.json | 32 +++++ .../codex_app_server_protocol.schemas.json | 32 +++++ .../codex_app_server_protocol.v2.schemas.json | 32 +++++ .../json/v2/ItemCompletedNotification.json | 32 +++++ .../json/v2/ItemStartedNotification.json | 32 +++++ .../schema/json/v2/ReviewStartResponse.json | 32 +++++ .../schema/json/v2/ThreadForkResponse.json | 32 +++++ .../schema/json/v2/ThreadListResponse.json | 32 +++++ .../json/v2/ThreadMetadataUpdateResponse.json | 32 +++++ .../schema/json/v2/ThreadReadResponse.json | 32 +++++ .../schema/json/v2/ThreadResumeResponse.json | 32 +++++ .../json/v2/ThreadRollbackResponse.json | 32 +++++ .../schema/json/v2/ThreadStartResponse.json | 32 +++++ .../json/v2/ThreadStartedNotification.json | 32 +++++ .../json/v2/ThreadUnarchiveResponse.json | 32 +++++ .../json/v2/TurnCompletedNotification.json | 32 +++++ .../schema/json/v2/TurnStartResponse.json | 32 +++++ .../json/v2/TurnStartedNotification.json | 32 +++++ .../schema/typescript/v2/ThreadItem.ts | 3 +- .../schema/typescript/v2/WebSearchResult.ts | 5 + .../schema/typescript/v2/index.ts | 1 + .../src/protocol/thread_history.rs | 21 +++ .../src/protocol/v2/item.rs | 23 ++++ .../src/protocol/v2/tests.rs | 11 ++ .../app-server/tests/suite/v2/web_search.rs | 2 + codex-rs/core/src/event_mapping.rs | 1 + codex-rs/core/src/event_mapping_tests.rs | 4 + .../src/tools/handlers/extension_tools.rs | 3 + .../src/event_processor_with_jsonl_output.rs | 12 ++ codex-rs/exec/src/exec_events.rs | 14 ++ codex-rs/exec/src/lib.rs | 1 + .../tests/event_processor_with_json_output.rs | 16 +++ codex-rs/ext/web-search/src/tool.rs | 1 + codex-rs/protocol/src/items.rs | 120 ++++++++++++++++++ codex-rs/protocol/src/legacy_events.rs | 1 + codex-rs/protocol/src/protocol.rs | 10 ++ codex-rs/tui/src/chatwidget/replay.rs | 4 +- 39 files changed, 831 insertions(+), 3 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/WebSearchResult.ts diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 0f759fd93862..e8c6de3ee425 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -4404,6 +4404,7 @@ async fn turn_event_counts_completed_tool_items() { id: "web-1".to_string(), query: "codex".to_string(), action: None, + results: None, }, ThreadItem::ImageGeneration { id: "image-1".to_string(), diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index 29f21bac52e6..6bef88f64205 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -2027,7 +2027,9 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option { }, )) } - ThreadItem::WebSearch { id, query, action } => { + ThreadItem::WebSearch { + id, query, action, .. + } => { let base = tool_item_base( thread_id, turn_id, diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index e025a61a3344..604feecd8c38 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -4511,6 +4511,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -5684,6 +5693,29 @@ } ] }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + }, "WindowsSandboxSetupCompletedNotification": { "properties": { "error": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index b7526b080da1..b9a1973f7c38 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -18357,6 +18357,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/v2/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -20829,6 +20838,29 @@ ], "type": "string" }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + }, "WebSearchToolConfig": { "additionalProperties": false, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index e1afc7883a51..62c556ba9f0d 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -16136,6 +16136,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -18608,6 +18617,29 @@ ], "type": "string" }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + }, "WebSearchToolConfig": { "additionalProperties": false, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index dcf6312c7d19..f2d94bf49b1a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -1102,6 +1102,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -1524,6 +1533,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index 1933225adef0..1b09c9523a5c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -1102,6 +1102,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -1524,6 +1533,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 6851ddb487d7..c8ebe56c3991 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -1247,6 +1247,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -1799,6 +1808,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 59a14cc070b1..cdecc36955e8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -1775,6 +1775,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -2405,6 +2414,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index e5cbc2e60606..20fd72fb2497 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -1566,6 +1566,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -2196,6 +2205,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index cdd1d47a6997..c941581baa8a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -1566,6 +1566,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -2196,6 +2205,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 613dd9648e79..c9087094febe 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -1566,6 +1566,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -2196,6 +2205,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 80fbb183f8a3..b7d79dc3c858 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -1775,6 +1775,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -2431,6 +2440,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 793f11c22617..182c826ea39a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -1566,6 +1566,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -2196,6 +2205,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 3ef1577038df..ac10822eded1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -1775,6 +1775,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -2405,6 +2414,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index a56d59c01df5..474b97c5fb01 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -1566,6 +1566,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -2196,6 +2205,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 6cc9b44cba3b..24ed2b91fa93 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -1566,6 +1566,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -2196,6 +2205,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 6ffb343595bf..98c2f5624824 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -1247,6 +1247,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -1799,6 +1808,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 805a5099a299..72aa1384c36e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -1247,6 +1247,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -1799,6 +1808,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 1f3f7c7036de..f4ba6807f02b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -1247,6 +1247,15 @@ "query": { "type": "string" }, + "results": { + "items": { + "$ref": "#/definitions/WebSearchResult" + }, + "type": [ + "array", + "null" + ] + }, "type": { "enum": [ "webSearch" @@ -1799,6 +1808,29 @@ "type": "object" } ] + }, + "WebSearchResult": { + "properties": { + "snippet": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index c611ab071c7e..397138b16aca 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -25,6 +25,7 @@ import type { PatchApplyStatus } from "./PatchApplyStatus"; import type { SubAgentActivityKind } from "./SubAgentActivityKind"; import type { UserInput } from "./UserInput"; import type { WebSearchAction } from "./WebSearchAction"; +import type { WebSearchResult } from "./WebSearchResult"; export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array, } | { "type": "hookPrompt", id: string, fragments: Array, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array, content: Array, } | { "type": "commandExecution", id: string, /** @@ -105,4 +106,4 @@ reasoningEffort: ReasoningEffort | null, /** * Last known status of the target agents, when available. */ -agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; +agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, results: Array | null, } | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/WebSearchResult.ts b/codex-rs/app-server-protocol/schema/typescript/v2/WebSearchResult.ts new file mode 100644 index 000000000000..aacf35d245b4 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/WebSearchResult.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WebSearchResult = { url: string, title: string | null, snippet: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 070d21eab820..f4b06a650d94 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -499,6 +499,7 @@ export type { TurnsPage } from "./TurnsPage"; export type { UserInput } from "./UserInput"; export type { WarningNotification } from "./WarningNotification"; export type { WebSearchAction } from "./WebSearchAction"; +export type { WebSearchResult } from "./WebSearchResult"; export type { WindowsSandboxReadiness } from "./WindowsSandboxReadiness"; export type { WindowsSandboxReadinessResponse } from "./WindowsSandboxReadinessResponse"; export type { WindowsSandboxSetupCompletedNotification } from "./WindowsSandboxSetupCompletedNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index 2d9c6b6a1a48..b9205a1460ee 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -22,6 +22,7 @@ use crate::protocol::v2::TurnItemsView; use crate::protocol::v2::TurnStatus; use crate::protocol::v2::UserInput; use crate::protocol::v2::WebSearchAction; +use crate::protocol::v2::WebSearchResult; use codex_protocol::items::parse_hook_prompt_message; use codex_protocol::models::MessagePhase; use codex_protocol::protocol::AgentReasoningEvent; @@ -615,6 +616,7 @@ impl ThreadHistoryBuilder { id: payload.call_id.clone(), query: String::new(), action: None, + results: None, }; self.upsert_item_in_current_turn(item); } @@ -624,6 +626,10 @@ impl ThreadHistoryBuilder { id: payload.call_id.clone(), query: payload.query.clone(), action: Some(WebSearchAction::from(payload.action.clone())), + results: payload + .results + .clone() + .map(|results| results.into_iter().map(WebSearchResult::from).collect()), }; self.upsert_item_in_current_turn(item); } @@ -1556,6 +1562,7 @@ mod tests { use codex_protocol::items::SleepItem as CoreSleepItem; use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::items::UserMessageItem as CoreUserMessageItem; + use codex_protocol::items::WebSearchResult as CoreWebSearchResult; use codex_protocol::items::build_hook_prompt_message; use codex_protocol::mcp::CallToolResult; use codex_protocol::models::ImageDetail; @@ -2443,6 +2450,11 @@ mod tests { query: Some("codex".into()), queries: None, }, + results: Some(vec![CoreWebSearchResult { + url: "https://example.com/codex".into(), + title: Some("Codex".into()), + snippet: Some("Codex documentation".into()), + }]), }), EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "exec-1".into(), @@ -2499,6 +2511,11 @@ mod tests { query: Some("codex".into()), queries: None, }), + results: Some(vec![WebSearchResult { + url: "https://example.com/codex".into(), + title: Some("Codex".into()), + snippet: Some("Codex documentation".into()), + }]), } ); assert_eq!( @@ -3917,6 +3934,7 @@ mod tests { query: Some("codex".into()), queries: None, }, + results: None, }), )); @@ -3932,6 +3950,7 @@ mod tests { query: Some("codex".into()), queries: None, }), + results: None, }, }], changed_turns: Vec::new(), @@ -4052,6 +4071,7 @@ mod tests { query: Some("codex".into()), queries: None, }, + results: None, })), ]); @@ -4067,6 +4087,7 @@ mod tests { query: Some("codex".into()), queries: None, }), + results: None, }, }], changed_turns: vec![ThreadHistoryTurnChange { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index cd04752449d1..d3db8afa6c5d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -359,6 +359,7 @@ pub enum ThreadItem { id: String, query: String, action: Option, + results: Option>, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -783,6 +784,25 @@ impl TryFrom for CoreGuardianAssessmentAction { } } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WebSearchResult { + pub url: String, + pub title: Option, + pub snippet: Option, +} + +impl From for WebSearchResult { + fn from(value: codex_protocol::items::WebSearchResult) -> Self { + Self { + url: value.url, + title: value.title, + snippet: value.snippet, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] #[ts(tag = "type", rename_all = "camelCase")] @@ -922,6 +942,9 @@ impl From for ThreadItem { id: search.id, query: search.query, action: Some(WebSearchAction::from(search.action)), + results: search + .results + .map(|results| results.into_iter().map(Into::into).collect()), }, CoreTurnItem::ImageView(image) => ThreadItem::ImageView { id: image.id, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 377ecba2c26c..0132137f276e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -20,6 +20,7 @@ use codex_protocol::items::SubAgentActivityItem; use codex_protocol::items::TurnItem; use codex_protocol::items::UserMessageItem; use codex_protocol::items::WebSearchItem; +use codex_protocol::items::WebSearchResult as CoreWebSearchResult; use codex_protocol::mcp::CallToolResult; use codex_protocol::mcp::McpServerInfo; use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation; @@ -2777,6 +2778,11 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { query: Some("docs".to_string()), queries: None, }, + results: Some(vec![CoreWebSearchResult { + url: "https://openai.com/docs".to_string(), + title: Some("OpenAI docs".to_string()), + snippet: Some("Documentation".to_string()), + }]), }); assert_eq!( @@ -2788,6 +2794,11 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { query: Some("docs".to_string()), queries: None, }), + results: Some(vec![WebSearchResult { + url: "https://openai.com/docs".to_string(), + title: Some("OpenAI docs".to_string()), + snippet: Some("Documentation".to_string()), + }]), } ); diff --git a/codex-rs/app-server/tests/suite/v2/web_search.rs b/codex-rs/app-server/tests/suite/v2/web_search.rs index f7d05395b4e5..f932c4c67dc3 100644 --- a/codex-rs/app-server/tests/suite/v2/web_search.rs +++ b/codex-rs/app-server/tests/suite/v2/web_search.rs @@ -187,6 +187,7 @@ async fn standalone_web_search_round_trips_output() -> Result<()> { id: call_id.to_string(), query: String::new(), action: Some(WebSearchAction::Other), + results: None, } ); let expected_completed_item = ThreadItem::WebSearch { @@ -196,6 +197,7 @@ async fn standalone_web_search_round_trips_output() -> Result<()> { query: Some("standalone web search".to_string()), queries: None, }), + results: None, }; assert_eq!(completed.item, expected_completed_item); diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index ef8f0fad5b4a..6eb89b104e05 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -204,6 +204,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option { id: id.clone().unwrap_or_default(), query, action, + results: None, })) } ResponseItem::ImageGenerationCall { diff --git a/codex-rs/core/src/event_mapping_tests.rs b/codex-rs/core/src/event_mapping_tests.rs index c91034d4518a..93195525d1fc 100644 --- a/codex-rs/core/src/event_mapping_tests.rs +++ b/codex-rs/core/src/event_mapping_tests.rs @@ -508,6 +508,7 @@ fn parses_web_search_call() { query: Some("weather".to_string()), queries: None, }, + results: None, } ), other => panic!("expected TurnItem::WebSearch, got {other:?}"), @@ -536,6 +537,7 @@ fn parses_web_search_open_page_call() { action: WebSearchAction::OpenPage { url: Some("https://example.com".to_string()), }, + results: None, } ), other => panic!("expected TurnItem::WebSearch, got {other:?}"), @@ -566,6 +568,7 @@ fn parses_web_search_find_in_page_call() { url: Some("https://example.com".to_string()), pattern: Some("needle".to_string()), }, + results: None, } ), other => panic!("expected TurnItem::WebSearch, got {other:?}"), @@ -589,6 +592,7 @@ fn parses_partial_web_search_call_without_action_as_other() { id: "ws_partial".to_string(), query: String::new(), action: WebSearchAction::Other, + results: None, } ), other => panic!("expected TurnItem::WebSearch, got {other:?}"), diff --git a/codex-rs/core/src/tools/handlers/extension_tools.rs b/codex-rs/core/src/tools/handlers/extension_tools.rs index 1aa7754a18f3..20373a19bee0 100644 --- a/codex-rs/core/src/tools/handlers/extension_tools.rs +++ b/codex-rs/core/src/tools/handlers/extension_tools.rs @@ -273,6 +273,7 @@ mod tests { query: Some("rust trait object".to_string()), queries: None, }, + results: None, }); call.turn_item_emitter.emit_started(item.clone()).await; call.turn_item_emitter.emit_completed(item).await; @@ -438,6 +439,7 @@ mod tests { query: Some("rust trait object".to_string()), queries: None, }, + results: None, }; assert_eq!(started_item, expected); assert_eq!(completed_item, expected); @@ -487,6 +489,7 @@ mod tests { id: "search-1".to_string(), query: "contributors".to_string(), action: WebSearchAction::Other, + results: None, }), ) .await; diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output.rs b/codex-rs/exec/src/event_processor_with_jsonl_output.rs index 79bf88eaaf69..880cb83ac69f 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output.rs @@ -54,6 +54,7 @@ use crate::exec_events::TurnFailedEvent; use crate::exec_events::TurnStartedEvent; use crate::exec_events::Usage; use crate::exec_events::WebSearchItem; +use crate::exec_events::WebSearchResult; pub struct EventProcessorWithJsonOutput { last_message_path: Option, @@ -297,6 +298,7 @@ impl EventProcessorWithJsonOutput { id: raw_id, query, action, + results, } => Some(ExecThreadItem { id: make_id(), details: ThreadItemDetails::WebSearch(WebSearchItem { @@ -309,6 +311,16 @@ impl EventProcessorWithJsonOutput { .unwrap_or(WebSearchAction::Other), None => WebSearchAction::Other, }, + results: results.map(|results| { + results + .into_iter() + .map(|result| WebSearchResult { + url: result.url, + title: result.title, + snippet: result.snippet, + }) + .collect() + }), }), }), _ => None, diff --git a/codex-rs/exec/src/exec_events.rs b/codex-rs/exec/src/exec_events.rs index 078a876ef488..ad0e3f62de5f 100644 --- a/codex-rs/exec/src/exec_events.rs +++ b/codex-rs/exec/src/exec_events.rs @@ -296,6 +296,20 @@ pub struct WebSearchItem { pub id: String, pub query: String, pub action: WebSearchAction, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub results: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] +pub struct WebSearchResult { + pub url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub snippet: Option, } /// An error notification. diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 7ea8461b00de..d3df04ce0ccc 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -138,6 +138,7 @@ pub use exec_events::TurnFailedEvent; pub use exec_events::TurnStartedEvent; pub use exec_events::Usage; pub use exec_events::WebSearchItem; +pub use exec_events::WebSearchResult; use serde_json::Value; use std::collections::HashMap; use std::future::Future; diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 1f44ca31a8cb..6fe9fd8d3b71 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -27,6 +27,7 @@ use codex_app_server_protocol::TurnPlanUpdatedNotification; use codex_app_server_protocol::TurnStartedNotification; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::WebSearchAction as ApiWebSearchAction; +use codex_app_server_protocol::WebSearchResult as ApiWebSearchResult; use codex_protocol::SessionId; use codex_protocol::ThreadId; use codex_protocol::models::PermissionProfile; @@ -74,6 +75,7 @@ use codex_exec::TurnFailedEvent; use codex_exec::TurnStartedEvent; use codex_exec::Usage; use codex_exec::WebSearchItem; +use codex_exec::WebSearchResult; #[test] fn map_todo_items_preserves_text_and_completion_state() { @@ -369,6 +371,11 @@ fn web_search_completion_preserves_query_and_action() { query: Some("rust async await".to_string()), queries: None, }), + results: Some(vec![ApiWebSearchResult { + url: "https://example.com/rust-async".to_string(), + title: Some("Async Rust".to_string()), + snippet: Some("An introduction to async Rust.".to_string()), + }]), }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), @@ -389,6 +396,11 @@ fn web_search_completion_preserves_query_and_action() { query: Some("rust async await".to_string()), queries: None, }, + results: Some(vec![WebSearchResult { + url: "https://example.com/rust-async".to_string(), + title: Some("Async Rust".to_string()), + snippet: Some("An introduction to async Rust.".to_string()), + }]), }), }, })], @@ -407,6 +419,7 @@ fn web_search_start_and_completion_reuse_item_id() { id: "search-1".to_string(), query: String::new(), action: None, + results: None, }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), @@ -422,6 +435,7 @@ fn web_search_start_and_completion_reuse_item_id() { query: Some("rust async await".to_string()), queries: None, }), + results: None, }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), @@ -439,6 +453,7 @@ fn web_search_start_and_completion_reuse_item_id() { id: "search-1".to_string(), query: String::new(), action: WebSearchAction::Other, + results: None, }), }, })], @@ -458,6 +473,7 @@ fn web_search_start_and_completion_reuse_item_id() { query: Some("rust async await".to_string()), queries: None, }, + results: None, }), }, })], diff --git a/codex-rs/ext/web-search/src/tool.rs b/codex-rs/ext/web-search/src/tool.rs index 9b8dbf56d6f5..e81270d921d7 100644 --- a/codex-rs/ext/web-search/src/tool.rs +++ b/codex-rs/ext/web-search/src/tool.rs @@ -185,6 +185,7 @@ fn web_search_item(call_id: &str, action: WebSearchAction) -> ExtensionTurnItem id: call_id.to_string(), query: web_search_action_detail(&action), action, + results: None, }) } diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index 2c9d073f0dce..5c4bfdbe7a62 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -22,6 +22,7 @@ use crate::user_input::TextElement; use crate::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; +use codex_utils_string::take_bytes_at_char_boundary; use quick_xml::de::from_str as from_xml_str; use quick_xml::se::to_string as to_xml_string; use schemars::JsonSchema; @@ -262,11 +263,97 @@ pub struct SubAgentActivityItem { pub agent_path: AgentPath, } +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] +pub struct WebSearchResult { + pub url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub snippet: Option, +} + +const MAX_WEB_SEARCH_RESULTS: usize = 20; +const MAX_WEB_SEARCH_RESULT_URL_BYTES: usize = 512; +const MAX_WEB_SEARCH_RESULT_TOTAL_URL_BYTES: usize = 2_048; +const MAX_WEB_SEARCH_RESULT_TITLE_BYTES: usize = 256; +const MAX_WEB_SEARCH_RESULT_SNIPPET_BYTES: usize = 512; +const MAX_WEB_SEARCH_RESULT_TOTAL_TEXT_BYTES: usize = 4_096; + +pub fn bounded_web_search_results( + results: impl IntoIterator, +) -> Vec { + let mut bounded_results = Vec::new(); + let mut total_url_bytes = 0usize; + let mut total_text_bytes = 0usize; + + for result in results { + if bounded_results.len() == MAX_WEB_SEARCH_RESULTS { + break; + } + if result.url.len() > MAX_WEB_SEARCH_RESULT_URL_BYTES + || total_url_bytes.saturating_add(result.url.len()) + > MAX_WEB_SEARCH_RESULT_TOTAL_URL_BYTES + { + continue; + } + + let title = bounded_web_search_result_text( + result.title, + MAX_WEB_SEARCH_RESULT_TITLE_BYTES, + MAX_WEB_SEARCH_RESULT_TOTAL_TEXT_BYTES.saturating_sub(total_text_bytes), + ); + total_text_bytes += title.as_ref().map_or(0, String::len); + let snippet = bounded_web_search_result_text( + result.snippet, + MAX_WEB_SEARCH_RESULT_SNIPPET_BYTES, + MAX_WEB_SEARCH_RESULT_TOTAL_TEXT_BYTES.saturating_sub(total_text_bytes), + ); + total_text_bytes += snippet.as_ref().map_or(0, String::len); + total_url_bytes += result.url.len(); + bounded_results.push(WebSearchResult { + url: result.url, + title, + snippet, + }); + } + + bounded_results +} + +fn bounded_web_search_result_text( + text: Option, + max_bytes: usize, + remaining_bytes: usize, +) -> Option { + let text = text?; + let text = text.trim(); + let text = take_bytes_at_char_boundary(text, max_bytes.min(remaining_bytes)); + (!text.is_empty()).then(|| text.to_string()) +} + +pub(crate) fn deserialize_web_search_results<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + Ok(Option::>::deserialize(deserializer)?.map(bounded_web_search_results)) +} + #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] pub struct WebSearchItem { pub id: String, pub query: String, pub action: WebSearchAction, + #[serde( + default, + deserialize_with = "deserialize_web_search_results", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional)] + pub results: Option>, } #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] @@ -649,4 +736,37 @@ mod tests { } ); } + + #[test] + fn bounds_web_search_result_text_and_total_payload() { + let results = bounded_web_search_results((0..20).map(|index| WebSearchResult { + url: format!("https://example.com/{index}"), + title: Some("t".repeat(300)), + snippet: Some("é".repeat(600)), + })); + + assert_eq!(results.len(), 20); + assert!( + results + .iter() + .filter_map(|result| result.title.as_ref()) + .all(|title| title.len() <= MAX_WEB_SEARCH_RESULT_TITLE_BYTES) + ); + assert!( + results + .iter() + .filter_map(|result| result.snippet.as_ref()) + .all(|snippet| snippet.len() <= MAX_WEB_SEARCH_RESULT_SNIPPET_BYTES) + ); + assert!( + results + .iter() + .map(|result| { + result.title.as_ref().map_or(0, String::len) + + result.snippet.as_ref().map_or(0, String::len) + }) + .sum::() + <= MAX_WEB_SEARCH_RESULT_TOTAL_TEXT_BYTES + ); + } } diff --git a/codex-rs/protocol/src/legacy_events.rs b/codex-rs/protocol/src/legacy_events.rs index c12a2747a29a..afe084e4db57 100644 --- a/codex-rs/protocol/src/legacy_events.rs +++ b/codex-rs/protocol/src/legacy_events.rs @@ -382,6 +382,7 @@ impl WebSearchItem { call_id: self.id.clone(), query: self.query.clone(), action: self.action.clone(), + results: self.results.clone(), }) } } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index b1b845fe2823..999fa6b1e656 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -32,6 +32,8 @@ use crate::dynamic_tools::DynamicToolCallRequest; use crate::dynamic_tools::DynamicToolResponse; use crate::dynamic_tools::DynamicToolSpec; use crate::items::TurnItem; +use crate::items::WebSearchResult; +use crate::items::deserialize_web_search_results; use crate::mcp::CallToolResult; use crate::mcp::RequestId; use crate::memory_citation::MemoryCitation; @@ -2408,6 +2410,13 @@ pub struct WebSearchEndEvent { pub call_id: String, pub query: String, pub action: WebSearchAction, + #[serde( + default, + deserialize_with = "deserialize_web_search_results", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional)] + pub results: Option>, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] @@ -5038,6 +5047,7 @@ mod tests { query: Some("find docs".into()), queries: None, }, + results: None, }), started_at_ms: 0, }; diff --git a/codex-rs/tui/src/chatwidget/replay.rs b/codex-rs/tui/src/chatwidget/replay.rs index 3ab5b3486be1..738e66648c9b 100644 --- a/codex-rs/tui/src/chatwidget/replay.rs +++ b/codex-rs/tui/src/chatwidget/replay.rs @@ -139,7 +139,9 @@ impl ChatWidget { .. } => self.on_mcp_tool_call_started(item), item @ ThreadItem::McpToolCall { .. } => self.on_mcp_tool_call_completed(item), - ThreadItem::WebSearch { id, query, action } => { + ThreadItem::WebSearch { + id, query, action, .. + } => { self.on_web_search_begin(id.clone()); self.on_web_search_end( id,