From 363636f5eb5c1ef6022638d4d63a04ce7d869e8f Mon Sep 17 00:00:00 2001
From: Reuben Narad <139025392+ReubenNarad@users.noreply.github.com>
Date: Sat, 23 Aug 2025 22:58:56 -0700
Subject: [PATCH] Add web search tool (#2371)
Adds web_search tool, enabling the model to use Responses API web_search
tool.
- Disabled by default, enabled by --search flag
- When --search is passed, exposes web_search_request function tool to
the model, which triggers user approval. When approved, the model can
use the web_search tool for the remainder of the turn
---------
Co-authored-by: easong-openai
---
codex-rs/core/src/chat_completions.rs | 6 +++
codex-rs/core/src/client.rs | 41 ++++++++++++++--
codex-rs/core/src/client_common.rs | 4 ++
codex-rs/core/src/codex.rs | 15 ++++++
codex-rs/core/src/config.rs | 30 ++++++++++--
codex-rs/core/src/openai_tools.rs | 48 +++++++++++++------
.../src/event_processor_with_human_output.rs | 4 ++
codex-rs/exec/src/lib.rs | 1 +
.../mcp-server/src/codex_message_processor.rs | 1 +
codex-rs/mcp-server/src/codex_tool_config.rs | 1 +
codex-rs/mcp-server/src/codex_tool_runner.rs | 1 +
codex-rs/protocol/src/protocol.rs | 8 ++++
codex-rs/tui/src/chatwidget.rs | 7 +++
codex-rs/tui/src/cli.rs | 4 ++
codex-rs/tui/src/history_cell.rs | 6 +++
codex-rs/tui/src/lib.rs | 7 +--
16 files changed, 158 insertions(+), 26 deletions(-)
diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs
index 55b20091..64157407 100644
--- a/codex-rs/core/src/chat_completions.rs
+++ b/codex-rs/core/src/chat_completions.rs
@@ -623,6 +623,12 @@ where
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryPartAdded))) => {
continue;
}
+ Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin { .. }))) => {
+ return Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin {
+ call_id: String::new(),
+ query: None,
+ })));
+ }
}
}
}
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index 6206b426..ecf1ae71 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -149,7 +149,21 @@ impl ModelClient {
let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT);
let full_instructions = prompt.get_full_instructions(&self.config.model_family);
- let tools_json = create_tools_json_for_responses_api(&prompt.tools)?;
+ let mut tools_json = create_tools_json_for_responses_api(&prompt.tools)?;
+ // ChatGPT backend expects the preview name for web search.
+ if auth_mode == Some(AuthMode::ChatGPT) {
+ for tool in &mut tools_json {
+ if let Some(map) = tool.as_object_mut()
+ && map.get("type").and_then(|v| v.as_str()) == Some("web_search")
+ {
+ map.insert(
+ "type".to_string(),
+ serde_json::Value::String("web_search_preview".to_string()),
+ );
+ }
+ }
+ }
+
let reasoning = create_reasoning_param_for_request(
&self.config.model_family,
self.effort,
@@ -466,7 +480,8 @@ async fn process_sse(
}
};
- trace!("SSE event: {}", sse.data);
+ let raw = sse.data.clone();
+ trace!("SSE event: {}", raw);
let event: SseEvent = match serde_json::from_str(&sse.data) {
Ok(event) => event,
@@ -580,8 +595,24 @@ async fn process_sse(
| "response.in_progress"
| "response.output_item.added"
| "response.output_text.done" => {
- // Currently, we ignore this event, but we handle it
- // separately to skip the logging message in the `other` case.
+ if event.kind == "response.output_item.added"
+ && let Some(item) = event.item.as_ref()
+ {
+ // Detect web_search_call begin and forward a synthetic event upstream.
+ if let Some(ty) = item.get("type").and_then(|v| v.as_str())
+ && ty == "web_search_call"
+ {
+ let call_id = item
+ .get("id")
+ .and_then(|v| v.as_str())
+ .unwrap_or("")
+ .to_string();
+ let ev = ResponseEvent::WebSearchCallBegin { call_id, query: None };
+ if tx_event.send(Ok(ev)).await.is_err() {
+ return;
+ }
+ }
+ }
}
"response.reasoning_summary_part.added" => {
// Boundary between reasoning summary sections (e.g., titles).
@@ -591,7 +622,7 @@ async fn process_sse(
}
}
"response.reasoning_summary_text.done" => {}
- other => debug!(other, "sse event"),
+ _ => {}
}
}
}
diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs
index c320d8d0..e2c191f4 100644
--- a/codex-rs/core/src/client_common.rs
+++ b/codex-rs/core/src/client_common.rs
@@ -93,6 +93,10 @@ pub enum ResponseEvent {
ReasoningSummaryDelta(String),
ReasoningContentDelta(String),
ReasoningSummaryPartAdded,
+ WebSearchCallBegin {
+ call_id: String,
+ query: Option,
+ },
}
#[derive(Debug, Serialize)]
diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index a7f8e7c3..e175f550 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -96,6 +96,7 @@ use crate::protocol::StreamErrorEvent;
use crate::protocol::Submission;
use crate::protocol::TaskCompleteEvent;
use crate::protocol::TurnDiffEvent;
+use crate::protocol::WebSearchBeginEvent;
use crate::rollout::RolloutRecorder;
use crate::safety::SafetyCheck;
use crate::safety::assess_command_safety;
@@ -511,6 +512,7 @@ impl Session {
sandbox_policy.clone(),
config.include_plan_tool,
config.include_apply_patch_tool,
+ config.tools_web_search_request,
config.use_experimental_streamable_shell_tool,
),
user_instructions,
@@ -1096,6 +1098,7 @@ async fn submission_loop(
new_sandbox_policy.clone(),
config.include_plan_tool,
config.include_apply_patch_tool,
+ config.tools_web_search_request,
config.use_experimental_streamable_shell_tool,
);
@@ -1175,6 +1178,7 @@ async fn submission_loop(
sandbox_policy.clone(),
config.include_plan_tool,
config.include_apply_patch_tool,
+ config.tools_web_search_request,
config.use_experimental_streamable_shell_tool,
),
user_instructions: turn_context.user_instructions.clone(),
@@ -1687,6 +1691,7 @@ async fn try_run_turn(
let mut stream = turn_context.client.clone().stream(&prompt).await?;
let mut output = Vec::new();
+
loop {
// Poll the next item from the model stream. We must inspect *both* Ok and Err
// cases so that transient stream failures (e.g., dropped SSE connection before
@@ -1723,6 +1728,16 @@ async fn try_run_turn(
.await?;
output.push(ProcessedResponseItem { item, response });
}
+ ResponseEvent::WebSearchCallBegin { call_id, query } => {
+ let q = query.unwrap_or_else(|| "Searching Web...".to_string());
+ let _ = sess
+ .tx_event
+ .send(Event {
+ id: sub_id.to_string(),
+ msg: EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id, query: q }),
+ })
+ .await;
+ }
ResponseEvent::Completed {
response_id: _,
token_usage,
diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs
index fbf0387a..98a8fde1 100644
--- a/codex-rs/core/src/config.rs
+++ b/codex-rs/core/src/config.rs
@@ -169,6 +169,8 @@ pub struct Config {
/// model family's default preference.
pub include_apply_patch_tool: bool,
+ pub tools_web_search_request: bool,
+
/// The value for the `originator` header included with Responses API requests.
pub responses_originator_header: String,
@@ -480,6 +482,9 @@ pub struct ConfigToml {
/// If set to `true`, the API key will be signed with the `originator` header.
pub preferred_auth_method: Option,
+
+ /// Nested tools section for feature toggles
+ pub tools: Option,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
@@ -487,6 +492,13 @@ pub struct ProjectConfig {
pub trust_level: Option,
}
+#[derive(Deserialize, Debug, Clone, Default)]
+pub struct ToolsToml {
+ // Renamed from `web_search_request`; keep alias for backwards compatibility.
+ #[serde(default, alias = "web_search_request")]
+ pub web_search: Option,
+}
+
impl ConfigToml {
/// Derive the effective sandbox policy from the configuration.
fn derive_sandbox_policy(&self, sandbox_mode_override: Option) -> SandboxPolicy {
@@ -576,6 +588,7 @@ pub struct ConfigOverrides {
pub include_apply_patch_tool: Option,
pub disable_response_storage: Option,
pub show_raw_agent_reasoning: Option,
+ pub tools_web_search_request: Option,
}
impl Config {
@@ -602,6 +615,7 @@ impl Config {
include_apply_patch_tool,
disable_response_storage,
show_raw_agent_reasoning,
+ tools_web_search_request: override_tools_web_search_request,
} = overrides;
let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) {
@@ -640,7 +654,7 @@ impl Config {
})?
.clone();
- let shell_environment_policy = cfg.shell_environment_policy.into();
+ let shell_environment_policy = cfg.shell_environment_policy.clone().into();
let resolved_cwd = {
use std::env;
@@ -661,7 +675,11 @@ impl Config {
}
};
- let history = cfg.history.unwrap_or_default();
+ let history = cfg.history.clone().unwrap_or_default();
+
+ let tools_web_search_request = override_tools_web_search_request
+ .or(cfg.tools.as_ref().and_then(|t| t.web_search))
+ .unwrap_or(false);
let model = model
.or(config_profile.model)
@@ -735,7 +753,7 @@ impl Config {
codex_home,
history,
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
- tui: cfg.tui.unwrap_or_default(),
+ tui: cfg.tui.clone().unwrap_or_default(),
codex_linux_sandbox_exe,
hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false),
@@ -754,12 +772,13 @@ impl Config {
model_verbosity: config_profile.model_verbosity.or(cfg.model_verbosity),
chatgpt_base_url: config_profile
.chatgpt_base_url
- .or(cfg.chatgpt_base_url)
+ .or(cfg.chatgpt_base_url.clone())
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
experimental_resume,
include_plan_tool: include_plan_tool.unwrap_or(false),
include_apply_patch_tool: include_apply_patch_tool.unwrap_or(false),
+ tools_web_search_request,
responses_originator_header,
preferred_auth_method: cfg.preferred_auth_method.unwrap_or(AuthMode::ChatGPT),
use_experimental_streamable_shell_tool: cfg
@@ -1129,6 +1148,7 @@ disable_response_storage = true
base_instructions: None,
include_plan_tool: false,
include_apply_patch_tool: false,
+ tools_web_search_request: false,
responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT,
use_experimental_streamable_shell_tool: false,
@@ -1184,6 +1204,7 @@ disable_response_storage = true
base_instructions: None,
include_plan_tool: false,
include_apply_patch_tool: false,
+ tools_web_search_request: false,
responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT,
use_experimental_streamable_shell_tool: false,
@@ -1254,6 +1275,7 @@ disable_response_storage = true
base_instructions: None,
include_plan_tool: false,
include_apply_patch_tool: false,
+ tools_web_search_request: false,
responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT,
use_experimental_streamable_shell_tool: false,
diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs
index 272c901d..516a9844 100644
--- a/codex-rs/core/src/openai_tools.rs
+++ b/codex-rs/core/src/openai_tools.rs
@@ -47,6 +47,8 @@ pub(crate) enum OpenAiTool {
Function(ResponsesApiTool),
#[serde(rename = "local_shell")]
LocalShell {},
+ #[serde(rename = "web_search")]
+ WebSearch {},
#[serde(rename = "custom")]
Freeform(FreeformTool),
}
@@ -64,6 +66,7 @@ pub struct ToolsConfig {
pub shell_type: ConfigShellToolType,
pub plan_tool: bool,
pub apply_patch_tool_type: Option,
+ pub web_search_request: bool,
}
impl ToolsConfig {
@@ -73,6 +76,7 @@ impl ToolsConfig {
sandbox_policy: SandboxPolicy,
include_plan_tool: bool,
include_apply_patch_tool: bool,
+ include_web_search_request: bool,
use_streamable_shell_tool: bool,
) -> Self {
let mut shell_type = if use_streamable_shell_tool {
@@ -104,6 +108,7 @@ impl ToolsConfig {
shell_type,
plan_tool: include_plan_tool,
apply_patch_tool_type,
+ web_search_request: include_web_search_request,
}
}
}
@@ -521,6 +526,10 @@ pub(crate) fn get_openai_tools(
}
}
+ if config.web_search_request {
+ tools.push(OpenAiTool::WebSearch {});
+ }
+
if let Some(mcp_tools) = mcp_tools {
for (name, tool) in mcp_tools {
match mcp_tool_to_openai_tool(name.clone(), tool.clone()) {
@@ -549,6 +558,7 @@ mod tests {
.map(|tool| match tool {
OpenAiTool::Function(ResponsesApiTool { name, .. }) => name,
OpenAiTool::LocalShell {} => "local_shell",
+ OpenAiTool::WebSearch {} => "web_search",
OpenAiTool::Freeform(FreeformTool { name, .. }) => name,
})
.collect::>();
@@ -576,11 +586,12 @@ mod tests {
SandboxPolicy::ReadOnly,
true,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
let tools = get_openai_tools(&config, Some(HashMap::new()));
- assert_eq_tool_names(&tools, &["local_shell", "update_plan"]);
+ assert_eq_tool_names(&tools, &["local_shell", "update_plan", "web_search"]);
}
#[test]
@@ -592,11 +603,12 @@ mod tests {
SandboxPolicy::ReadOnly,
true,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
let tools = get_openai_tools(&config, Some(HashMap::new()));
- assert_eq_tool_names(&tools, &["shell", "update_plan"]);
+ assert_eq_tool_names(&tools, &["shell", "update_plan", "web_search"]);
}
#[test]
@@ -608,6 +620,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
let tools = get_openai_tools(
@@ -631,8 +644,8 @@ mod tests {
"number_property": { "type": "number" },
},
"required": [
- "string_property",
- "number_property"
+ "string_property".to_string(),
+ "number_property".to_string()
],
"additionalProperties": Some(false),
},
@@ -648,10 +661,13 @@ mod tests {
)])),
);
- assert_eq_tool_names(&tools, &["shell", "test_server/do_something_cool"]);
+ assert_eq_tool_names(
+ &tools,
+ &["shell", "web_search", "test_server/do_something_cool"],
+ );
assert_eq!(
- tools[1],
+ tools[2],
OpenAiTool::Function(ResponsesApiTool {
name: "test_server/do_something_cool".to_string(),
parameters: JsonSchema::Object {
@@ -703,6 +719,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
@@ -729,10 +746,10 @@ mod tests {
)])),
);
- assert_eq_tool_names(&tools, &["shell", "dash/search"]);
+ assert_eq_tool_names(&tools, &["shell", "web_search", "dash/search"]);
assert_eq!(
- tools[1],
+ tools[2],
OpenAiTool::Function(ResponsesApiTool {
name: "dash/search".to_string(),
parameters: JsonSchema::Object {
@@ -760,6 +777,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
@@ -784,9 +802,9 @@ mod tests {
)])),
);
- assert_eq_tool_names(&tools, &["shell", "dash/paginate"]);
+ assert_eq_tool_names(&tools, &["shell", "web_search", "dash/paginate"]);
assert_eq!(
- tools[1],
+ tools[2],
OpenAiTool::Function(ResponsesApiTool {
name: "dash/paginate".to_string(),
parameters: JsonSchema::Object {
@@ -812,6 +830,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
@@ -836,9 +855,9 @@ mod tests {
)])),
);
- assert_eq_tool_names(&tools, &["shell", "dash/tags"]);
+ assert_eq_tool_names(&tools, &["shell", "web_search", "dash/tags"]);
assert_eq!(
- tools[1],
+ tools[2],
OpenAiTool::Function(ResponsesApiTool {
name: "dash/tags".to_string(),
parameters: JsonSchema::Object {
@@ -867,6 +886,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
@@ -891,9 +911,9 @@ mod tests {
)])),
);
- assert_eq_tool_names(&tools, &["shell", "dash/value"]);
+ assert_eq_tool_names(&tools, &["shell", "web_search", "dash/value"]);
assert_eq!(
- tools[1],
+ tools[2],
OpenAiTool::Function(ResponsesApiTool {
name: "dash/value".to_string(),
parameters: JsonSchema::Object {
diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs
index 0f7e14ea..cfdba984 100644
--- a/codex-rs/exec/src/event_processor_with_human_output.rs
+++ b/codex-rs/exec/src/event_processor_with_human_output.rs
@@ -24,6 +24,7 @@ use codex_core::protocol::StreamErrorEvent;
use codex_core::protocol::TaskCompleteEvent;
use codex_core::protocol::TurnAbortReason;
use codex_core::protocol::TurnDiffEvent;
+use codex_core::protocol::WebSearchBeginEvent;
use owo_colors::OwoColorize;
use owo_colors::Style;
use shlex::try_join;
@@ -361,6 +362,9 @@ impl EventProcessor for EventProcessorWithHumanOutput {
}
}
}
+ EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id: _, query }) => {
+ ts_println!(self, "🌐 {query}");
+ }
EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
call_id,
auto_approved,
diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs
index d403cb79..3de95291 100644
--- a/codex-rs/exec/src/lib.rs
+++ b/codex-rs/exec/src/lib.rs
@@ -150,6 +150,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any
include_apply_patch_tool: None,
disable_response_storage: oss.then_some(true),
show_raw_agent_reasoning: oss.then_some(true),
+ tools_web_search_request: None,
};
// Parse `-c` overrides.
let cli_kv_overrides = match config_overrides.parse_overrides() {
diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs
index 0bbf6ff8..97a3602d 100644
--- a/codex-rs/mcp-server/src/codex_message_processor.rs
+++ b/codex-rs/mcp-server/src/codex_message_processor.rs
@@ -738,6 +738,7 @@ fn derive_config_from_params(
include_apply_patch_tool,
disable_response_storage: None,
show_raw_agent_reasoning: None,
+ tools_web_search_request: None,
};
let cli_overrides = cli_overrides
diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs
index 5993c10f..69f07ff2 100644
--- a/codex-rs/mcp-server/src/codex_tool_config.rs
+++ b/codex-rs/mcp-server/src/codex_tool_config.rs
@@ -163,6 +163,7 @@ impl CodexToolCallParam {
include_apply_patch_tool: None,
disable_response_storage: None,
show_raw_agent_reasoning: None,
+ tools_web_search_request: None,
};
let cli_overrides = cli_overrides
diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs
index c6d65bc8..8480e29c 100644
--- a/codex-rs/mcp-server/src/codex_tool_runner.rs
+++ b/codex-rs/mcp-server/src/codex_tool_runner.rs
@@ -272,6 +272,7 @@ async fn run_codex_tool_session_inner(
| EventMsg::PatchApplyBegin(_)
| EventMsg::PatchApplyEnd(_)
| EventMsg::TurnDiff(_)
+ | EventMsg::WebSearchBegin(_)
| EventMsg::GetHistoryEntryResponse(_)
| EventMsg::PlanUpdate(_)
| EventMsg::TurnAborted(_)
diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs
index 7e7708b2..71c15381 100644
--- a/codex-rs/protocol/src/protocol.rs
+++ b/codex-rs/protocol/src/protocol.rs
@@ -437,6 +437,8 @@ pub enum EventMsg {
McpToolCallEnd(McpToolCallEndEvent),
+ WebSearchBegin(WebSearchBeginEvent),
+
/// Notification that the server is about to execute a command.
ExecCommandBegin(ExecCommandBeginEvent),
@@ -658,6 +660,12 @@ impl McpToolCallEndEvent {
}
}
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct WebSearchBeginEvent {
+ pub call_id: String,
+ pub query: String,
+}
+
/// Response payload for `Op::GetHistory` containing the current session's
/// in-memory transcript.
#[derive(Debug, Clone, Deserialize, Serialize)]
diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
index 235280be..5f48b0e4 100644
--- a/codex-rs/tui/src/chatwidget.rs
+++ b/codex-rs/tui/src/chatwidget.rs
@@ -28,6 +28,7 @@ use codex_core::protocol::TaskCompleteEvent;
use codex_core::protocol::TokenUsage;
use codex_core::protocol::TurnAbortReason;
use codex_core::protocol::TurnDiffEvent;
+use codex_core::protocol::WebSearchBeginEvent;
use codex_protocol::parse_command::ParsedCommand;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
@@ -308,6 +309,11 @@ impl ChatWidget {
self.defer_or_handle(|q| q.push_mcp_end(ev), |s| s.handle_mcp_end_now(ev2));
}
+ fn on_web_search_begin(&mut self, ev: WebSearchBeginEvent) {
+ self.flush_answer_stream_with_separator();
+ self.add_to_history(history_cell::new_web_search_call(ev.query));
+ }
+
fn on_get_history_entry_response(
&mut self,
event: codex_core::protocol::GetHistoryEntryResponseEvent,
@@ -839,6 +845,7 @@ impl ChatWidget {
EventMsg::ExecCommandEnd(ev) => self.on_exec_command_end(ev),
EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev),
EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev),
+ EventMsg::WebSearchBegin(ev) => self.on_web_search_begin(ev),
EventMsg::GetHistoryEntryResponse(ev) => self.on_get_history_entry_response(ev),
EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev),
EventMsg::ShutdownComplete => self.on_shutdown_complete(),
diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs
index 91ee9cfd..8eb6d6b8 100644
--- a/codex-rs/tui/src/cli.rs
+++ b/codex-rs/tui/src/cli.rs
@@ -54,6 +54,10 @@ pub struct Cli {
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option,
+ /// Enable web search (off by default). When enabled, the native Responses `web_search` tool is available to the model (no per‑call approval).
+ #[arg(long = "search", default_value_t = false)]
+ pub web_search: bool,
+
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
}
diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs
index 3e51ffd0..0b2af7a1 100644
--- a/codex-rs/tui/src/history_cell.rs
+++ b/codex-rs/tui/src/history_cell.rs
@@ -445,6 +445,12 @@ pub(crate) fn new_active_mcp_tool_call(invocation: McpInvocation) -> PlainHistor
PlainHistoryCell { lines }
}
+pub(crate) fn new_web_search_call(query: String) -> PlainHistoryCell {
+ let lines: Vec> =
+ vec![Line::from(""), Line::from(vec!["🌐 ".into(), query.into()])];
+ PlainHistoryCell { lines }
+}
+
/// If the first content is an image, return a new cell with the image.
/// TODO(rgwood-dd): Handle images properly even if they're not the first result.
fn try_new_completed_mcp_tool_call_with_image_output(
diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs
index d586c202..e5dc5f2a 100644
--- a/codex-rs/tui/src/lib.rs
+++ b/codex-rs/tui/src/lib.rs
@@ -128,10 +128,11 @@ pub async fn run_main(
include_apply_patch_tool: None,
disable_response_storage: cli.oss.then_some(true),
show_raw_agent_reasoning: cli.oss.then_some(true),
+ tools_web_search_request: cli.web_search.then_some(true),
};
-
- // Parse `-c` overrides from the CLI.
- let cli_kv_overrides = match cli.config_overrides.parse_overrides() {
+ let raw_overrides = cli.config_overrides.raw_overrides.clone();
+ let overrides_cli = codex_common::CliConfigOverrides { raw_overrides };
+ let cli_kv_overrides = match overrides_cli.parse_overrides() {
Ok(v) => v,
#[allow(clippy::print_stderr)]
Err(e) => {