fix: handle all web_search actions and in progress invocations (#9960)

### Summary
- Parse all `web_search` tool actions (`search`, `find_in_page`,
`open_page`).
- Previously we only parsed + displayed `search`, which made the TUI
appear to pause when the other actions were being used.
- Show in progress `web_search` calls as `Searching the web`
  - Previously we only showed completed tool calls

<img width="308" height="149" alt="image"
src="https://github.com/user-attachments/assets/90a4e8ff-b06a-48ff-a282-b57b31121845"
/>

### Tests
Added + updated tests, tested locally

### Follow ups
Update VSCode extension to display these as well
This commit is contained in:
sayan-oai
2026-01-26 19:33:48 -08:00
committed by GitHub
parent 998e88b12a
commit 86adf53235
18 changed files with 462 additions and 62 deletions

View File

@@ -157,7 +157,9 @@ pub enum ResponseItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
status: Option<String>,
action: WebSearchAction,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
action: Option<WebSearchAction>,
},
// Generated by the harness but considered exactly as a model response.
GhostSnapshot {
@@ -1034,10 +1036,12 @@ mod tests {
"query": "weather seattle"
}
}"#,
WebSearchAction::Search {
None,
Some(WebSearchAction::Search {
query: Some("weather seattle".into()),
},
}),
Some("completed".into()),
true,
),
(
r#"{
@@ -1048,10 +1052,12 @@ mod tests {
"url": "https://example.com"
}
}"#,
WebSearchAction::OpenPage {
None,
Some(WebSearchAction::OpenPage {
url: Some("https://example.com".into()),
},
}),
Some("open".into()),
true,
),
(
r#"{
@@ -1063,26 +1069,43 @@ mod tests {
"pattern": "installation"
}
}"#,
WebSearchAction::FindInPage {
None,
Some(WebSearchAction::FindInPage {
url: Some("https://example.com/docs".into()),
pattern: Some("installation".into()),
},
}),
Some("in_progress".into()),
true,
),
(
r#"{
"type": "web_search_call",
"status": "in_progress",
"id": "ws_partial"
}"#,
Some("ws_partial".into()),
None,
Some("in_progress".into()),
false,
),
];
for (json_literal, expected_action, expected_status) in cases {
for (json_literal, expected_id, expected_action, expected_status, expect_roundtrip) in cases
{
let parsed: ResponseItem = serde_json::from_str(json_literal)?;
let expected = ResponseItem::WebSearchCall {
id: None,
id: expected_id.clone(),
status: expected_status.clone(),
action: expected_action.clone(),
};
assert_eq!(parsed, expected);
let serialized = serde_json::to_value(&parsed)?;
let original_value: serde_json::Value = serde_json::from_str(json_literal)?;
assert_eq!(serialized, original_value);
let mut expected_serialized: serde_json::Value = serde_json::from_str(json_literal)?;
if !expect_roundtrip && let Some(obj) = expected_serialized.as_object_mut() {
obj.remove("id");
}
assert_eq!(serialized, expected_serialized);
}
Ok(())