From 0fff60e88ba5317f9e3a524edfedcd04ebdebd47 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 14:18:46 -0800 Subject: [PATCH] Gate default-mode request_user_input behind new feature --- codex-rs/app-server/src/message_processor.rs | 3 + .../tests/suite/v2/request_user_input.rs | 3 - .../app-server/tests/suite/v2/turn_start.rs | 2 +- codex-rs/core/src/codex.rs | 3 + codex-rs/core/src/features.rs | 8 +++ .../collaboration_mode_presets.rs | 34 +++++++---- codex-rs/core/src/models_manager/manager.rs | 16 ++++-- codex-rs/core/src/test_support.rs | 2 +- codex-rs/core/src/thread_manager.rs | 2 + .../src/tools/handlers/request_user_input.rs | 57 +++++++++++++------ codex-rs/core/src/tools/spec.rs | 56 +++++++----------- codex-rs/core/tests/suite/client.rs | 3 + .../core/tests/suite/model_info_overrides.rs | 4 +- codex-rs/core/tests/suite/model_tools.rs | 2 - .../core/tests/suite/request_user_input.rs | 30 ++++++---- codex-rs/exec/src/lib.rs | 3 + codex-rs/mcp-server/src/message_processor.rs | 3 + codex-rs/protocol/src/config_types.rs | 2 +- codex-rs/tui/src/app.rs | 3 + codex-rs/tui/src/chatwidget/tests.rs | 8 ++- 20 files changed, 157 insertions(+), 87 deletions(-) diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 18d6a121e7..39a8719dc0 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -177,6 +177,9 @@ impl MessageProcessor { auth_manager.clone(), SessionSource::VSCode, config.model_catalog.clone(), + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), )); let cloud_requirements = Arc::new(RwLock::new(cloud_requirements)); let codex_message_processor = CodexMessageProcessor::new(CodexMessageProcessorArgs { diff --git a/codex-rs/app-server/tests/suite/v2/request_user_input.rs b/codex-rs/app-server/tests/suite/v2/request_user_input.rs index 4ee76bdca2..926cb9bbc6 100644 --- a/codex-rs/app-server/tests/suite/v2/request_user_input.rs +++ b/codex-rs/app-server/tests/suite/v2/request_user_input.rs @@ -123,9 +123,6 @@ sandbox_mode = "read-only" model_provider = "mock_provider" -[features] -collaboration_modes = true - [model_providers.mock_provider] name = "Mock provider for test" base_url = "{server_uri}/v1" diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index d7e098c2f8..48d5157c14 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -352,7 +352,7 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { codex_home.path(), &server.uri(), "never", - &BTreeMap::default(), + &BTreeMap::from([(Feature::DefaultModeRequestUserInput, true)]), )?; let mut mcp = McpProcess::new(codex_home.path()).await?; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 397318c8e8..2a8a956ade 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -8059,6 +8059,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, + false, )); let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); let model_info = @@ -8134,6 +8135,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, + false, )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); @@ -8289,6 +8291,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, + false, )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index d3bace8519..ed1734185b 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -137,6 +137,8 @@ pub enum Feature { SkillApproval, /// Steer feature flag - when enabled, Enter submits immediately instead of queuing. Steer, + /// Allow request_user_input in Default collaboration mode. + DefaultModeRequestUserInput, /// Enable collaboration modes (Plan, Default). /// Kept for config backward compatibility; behavior is always collaboration-modes-enabled. CollaborationModes, @@ -635,6 +637,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::DefaultModeRequestUserInput, + key: "default_mode_request_user_input", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::CollaborationModes, key: "collaboration_modes", diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs index 7b900609b3..3eed6827e5 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -9,8 +9,13 @@ const COLLABORATION_MODE_DEFAULT: &str = const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; -pub(crate) fn builtin_collaboration_mode_presets() -> Vec { - vec![plan_preset(), default_preset()] +pub(crate) fn builtin_collaboration_mode_presets( + default_mode_request_user_input: bool, +) -> Vec { + vec![ + plan_preset(), + default_preset(default_mode_request_user_input), + ] } fn plan_preset() -> CollaborationModeMask { @@ -23,20 +28,22 @@ fn plan_preset() -> CollaborationModeMask { } } -fn default_preset() -> CollaborationModeMask { +fn default_preset(default_mode_request_user_input: bool) -> CollaborationModeMask { CollaborationModeMask { name: ModeKind::Default.display_name().to_string(), mode: Some(ModeKind::Default), model: None, reasoning_effort: None, - developer_instructions: Some(Some(default_mode_instructions())), + developer_instructions: Some(Some(default_mode_instructions( + default_mode_request_user_input, + ))), } } -fn default_mode_instructions() -> String { +fn default_mode_instructions(default_mode_request_user_input: bool) -> String { let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); let request_user_input_availability = - request_user_input_availability_message(ModeKind::Default); + request_user_input_availability_message(ModeKind::Default, default_mode_request_user_input); COLLABORATION_MODE_DEFAULT .replace(KNOWN_MODE_NAMES_PLACEHOLDER, &known_mode_names) .replace( @@ -55,9 +62,14 @@ fn format_mode_names(modes: &[ModeKind]) -> String { } } -fn request_user_input_availability_message(mode: ModeKind) -> String { +fn request_user_input_availability_message( + mode: ModeKind, + default_mode_request_user_input: bool, +) -> String { let mode_name = mode.display_name(); - if mode.allows_request_user_input() { + if mode.allows_request_user_input() + || (default_mode_request_user_input && mode == ModeKind::Default) + { format!("The `request_user_input` tool is available in {mode_name} mode.") } else { format!( @@ -74,7 +86,7 @@ mod tests { #[test] fn preset_names_use_mode_display_names() { assert_eq!(plan_preset().name, ModeKind::Plan.display_name()); - assert_eq!(default_preset().name, ModeKind::Default.display_name()); + assert_eq!(default_preset(false).name, ModeKind::Default.display_name()); assert_eq!( plan_preset().reasoning_effort, Some(Some(ReasoningEffort::Medium)) @@ -83,7 +95,7 @@ mod tests { #[test] fn default_mode_instructions_replace_mode_names_placeholder() { - let default_instructions = default_preset() + let default_instructions = default_preset(true) .developer_instructions .expect("default preset should include instructions") .expect("default instructions should be set"); @@ -96,7 +108,7 @@ mod tests { assert!(default_instructions.contains(&expected_snippet)); let expected_availability_message = - request_user_input_availability_message(ModeKind::Default); + request_user_input_availability_message(ModeKind::Default, true); assert!(default_instructions.contains(&expected_availability_message)); } } diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index 4b1da3e58a..fd9e1a017f 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -46,6 +46,7 @@ pub enum RefreshStrategy { pub struct ModelsManager { remote_models: RwLock>, has_custom_model_catalog: bool, + default_mode_request_user_input: bool, auth_manager: Arc, etag: RwLock>, cache_manager: ModelsCacheManager, @@ -62,6 +63,7 @@ impl ModelsManager { codex_home: PathBuf, auth_manager: Arc, model_catalog: Option, + default_mode_request_user_input: bool, ) -> Self { let cache_path = codex_home.join(MODEL_CACHE_FILE); let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL); @@ -75,6 +77,7 @@ impl ModelsManager { Self { remote_models: RwLock::new(remote_models), has_custom_model_catalog, + default_mode_request_user_input, auth_manager, etag: RwLock::new(None), cache_manager, @@ -97,7 +100,7 @@ impl ModelsManager { /// /// Returns a static set of presets seeded with the configured model. pub fn list_collaboration_modes(&self) -> Vec { - builtin_collaboration_mode_presets() + builtin_collaboration_mode_presets(self.default_mode_request_user_input) } /// Attempt to list models without blocking, using the current cached state. @@ -365,6 +368,7 @@ impl ModelsManager { .unwrap_or_else(|err| panic!("failed to load bundled models.json: {err}")), ), has_custom_model_catalog: false, + default_mode_request_user_input: false, auth_manager, etag: RwLock::new(None), cache_manager, @@ -491,7 +495,8 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None); + let manager = + ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None, false); let known_slug = manager .get_remote_models() .await @@ -528,6 +533,7 @@ mod tests { Some(ModelsResponse { models: vec![remote_model("gpt-overlay", "Overlay", 0)], }), + false, ); let model_info = manager @@ -551,7 +557,8 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None); + let manager = + ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None, false); let known_slug = manager .get_remote_models() .await @@ -577,7 +584,8 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None); + let manager = + ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None, false); let known_slug = manager .get_remote_models() .await diff --git a/codex-rs/core/src/test_support.rs b/codex-rs/core/src/test_support.rs index 5e6d611adc..3084d92fa8 100644 --- a/codex-rs/core/src/test_support.rs +++ b/codex-rs/core/src/test_support.rs @@ -85,5 +85,5 @@ pub fn all_model_presets() -> &'static Vec { } pub fn builtin_collaboration_mode_presets() -> Vec { - collaboration_mode_presets::builtin_collaboration_mode_presets() + collaboration_mode_presets::builtin_collaboration_mode_presets(false) } diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index cb7c464362..8db3bd371f 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -143,6 +143,7 @@ impl ThreadManager { auth_manager: Arc, session_source: SessionSource, model_catalog: Option, + default_mode_request_user_input: bool, ) -> Self { let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY); let skills_manager = Arc::new(SkillsManager::new(codex_home.clone())); @@ -155,6 +156,7 @@ impl ThreadManager { codex_home, auth_manager.clone(), model_catalog, + default_mode_request_user_input, )), skills_manager, file_watcher, diff --git a/codex-rs/core/src/tools/handlers/request_user_input.rs b/codex-rs/core/src/tools/handlers/request_user_input.rs index 06ce5d6f78..f9b444186e 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input.rs @@ -12,10 +12,15 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES; use codex_protocol::request_user_input::RequestUserInputArgs; -fn format_allowed_modes() -> String { +fn request_user_input_is_available(mode: ModeKind, default_mode_request_user_input: bool) -> bool { + mode.allows_request_user_input() + || (default_mode_request_user_input && mode == ModeKind::Default) +} + +fn format_allowed_modes(default_mode_request_user_input: bool) -> String { let mode_names: Vec<&str> = TUI_VISIBLE_COLLABORATION_MODES .into_iter() - .filter(|mode| mode.allows_request_user_input()) + .filter(|mode| request_user_input_is_available(*mode, default_mode_request_user_input)) .map(ModeKind::display_name) .collect(); @@ -27,8 +32,11 @@ fn format_allowed_modes() -> String { } } -pub(crate) fn request_user_input_unavailable_message(mode: ModeKind) -> Option { - if mode.allows_request_user_input() { +pub(crate) fn request_user_input_unavailable_message( + mode: ModeKind, + default_mode_request_user_input: bool, +) -> Option { + if request_user_input_is_available(mode, default_mode_request_user_input) { None } else { let mode_name = mode.display_name(); @@ -38,14 +46,16 @@ pub(crate) fn request_user_input_unavailable_message(mode: ModeKind) -> Option String { - let allowed_modes = format_allowed_modes(); +pub(crate) fn request_user_input_tool_description(default_mode_request_user_input: bool) -> String { + let allowed_modes = format_allowed_modes(default_mode_request_user_input); format!( "Request user input for one to three short questions and wait for the response. This tool is only available in {allowed_modes}." ) } -pub struct RequestUserInputHandler; +pub struct RequestUserInputHandler { + pub default_mode_request_user_input: bool, +} #[async_trait] impl ToolHandler for RequestUserInputHandler { @@ -72,7 +82,9 @@ impl ToolHandler for RequestUserInputHandler { }; let mode = session.collaboration_mode().await.mode; - if let Some(message) = request_user_input_unavailable_message(mode) { + if let Some(message) = + request_user_input_unavailable_message(mode, self.default_mode_request_user_input) + { return Err(FunctionCallError::RespondToModel(message)); } @@ -117,34 +129,45 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn request_user_input_mode_availability_includes_default_mode() { + fn request_user_input_mode_availability_defaults_to_plan_only() { assert!(ModeKind::Plan.allows_request_user_input()); - assert!(ModeKind::Default.allows_request_user_input()); + assert!(!ModeKind::Default.allows_request_user_input()); assert!(!ModeKind::Execute.allows_request_user_input()); assert!(!ModeKind::PairProgramming.allows_request_user_input()); } #[test] - fn request_user_input_unavailable_messages_use_default_name_for_default_modes() { - assert_eq!(request_user_input_unavailable_message(ModeKind::Plan), None); + fn request_user_input_unavailable_messages_respect_default_mode_feature_flag() { assert_eq!( - request_user_input_unavailable_message(ModeKind::Default), + request_user_input_unavailable_message(ModeKind::Plan, false), None ); assert_eq!( - request_user_input_unavailable_message(ModeKind::Execute), + request_user_input_unavailable_message(ModeKind::Default, false), + Some("request_user_input is unavailable in Default mode".to_string()) + ); + assert_eq!( + request_user_input_unavailable_message(ModeKind::Default, true), + None + ); + assert_eq!( + request_user_input_unavailable_message(ModeKind::Execute, false), Some("request_user_input is unavailable in Execute mode".to_string()) ); assert_eq!( - request_user_input_unavailable_message(ModeKind::PairProgramming), + request_user_input_unavailable_message(ModeKind::PairProgramming, false), Some("request_user_input is unavailable in Pair Programming mode".to_string()) ); } #[test] - fn request_user_input_tool_description_mentions_default_and_plan() { + fn request_user_input_tool_description_mentions_available_modes() { assert_eq!( - request_user_input_tool_description(), + request_user_input_tool_description(false), + "Request user input for one to three short questions and wait for the response. This tool is only available in Plan mode.".to_string() + ); + assert_eq!( + request_user_input_tool_description(true), "Request user input for one to three short questions and wait for the response. This tool is only available in Default or Plan mode.".to_string() ); } diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 3c9ab4bf3a..7c4b84b4c9 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -54,7 +54,7 @@ pub(crate) struct ToolsConfig { pub js_repl_enabled: bool, pub js_repl_tools_only: bool, pub collab_tools: bool, - pub collaboration_modes_tools: bool, + pub default_mode_request_user_input: bool, pub experimental_supported_tools: Vec, pub agent_jobs_tools: bool, pub agent_jobs_worker_tools: bool, @@ -80,7 +80,8 @@ impl ToolsConfig { let include_js_repl_tools_only = include_js_repl && features.enabled(Feature::JsReplToolsOnly); let include_collab_tools = features.enabled(Feature::Collab); - let include_collaboration_modes_tools = features.enabled(Feature::CollaborationModes); + let include_default_mode_request_user_input = + features.enabled(Feature::DefaultModeRequestUserInput); let include_search_tool = features.enabled(Feature::Apps); let include_agent_jobs = include_collab_tools && features.enabled(Feature::Sqlite); let request_permission_enabled = features.enabled(Feature::RequestPermissions); @@ -137,7 +138,7 @@ impl ToolsConfig { js_repl_enabled: include_js_repl, js_repl_tools_only: include_js_repl_tools_only, collab_tools: include_collab_tools, - collaboration_modes_tools: include_collaboration_modes_tools, + default_mode_request_user_input: include_default_mode_request_user_input, experimental_supported_tools: model_info.experimental_supported_tools.clone(), agent_jobs_tools: include_agent_jobs, agent_jobs_worker_tools, @@ -865,7 +866,7 @@ fn create_wait_tool() -> ToolSpec { }) } -fn create_request_user_input_tool() -> ToolSpec { +fn create_request_user_input_tool(default_mode_request_user_input: bool) -> ToolSpec { let mut option_props = BTreeMap::new(); option_props.insert( "label".to_string(), @@ -936,7 +937,7 @@ fn create_request_user_input_tool() -> ToolSpec { ToolSpec::Function(ResponsesApiTool { name: "request_user_input".to_string(), - description: request_user_input_tool_description(), + description: request_user_input_tool_description(default_mode_request_user_input), strict: false, parameters: JsonSchema::Object { properties, @@ -1657,7 +1658,9 @@ pub(crate) fn build_specs( let mcp_handler = Arc::new(McpHandler); let mcp_resource_handler = Arc::new(McpResourceHandler); let shell_command_handler = Arc::new(ShellCommandHandler::from(config.shell_command_backend)); - let request_user_input_handler = Arc::new(RequestUserInputHandler); + let request_user_input_handler = Arc::new(RequestUserInputHandler { + default_mode_request_user_input: config.default_mode_request_user_input, + }); let search_tool_handler = Arc::new(SearchToolBm25Handler); let js_repl_handler = Arc::new(JsReplHandler); let js_repl_reset_handler = Arc::new(JsReplResetHandler); @@ -1720,10 +1723,10 @@ pub(crate) fn build_specs( builder.register_handler("js_repl_reset", js_repl_reset_handler); } - if config.collaboration_modes_tools { - builder.push_spec(create_request_user_input_tool()); - builder.register_handler("request_user_input", request_user_input_handler); - } + builder.push_spec(create_request_user_input_tool( + config.default_mode_request_user_input, + )); + builder.register_handler("request_user_input", request_user_input_handler); if config.search_tool && let Some(app_tools) = app_tools @@ -2017,7 +2020,6 @@ mod tests { let model_info = model_info_from_models_json("gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); let config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2048,7 +2050,7 @@ mod tests { create_exec_command_tool(true, false), create_write_stdin_tool(), PLAN_TOOL.clone(), - create_request_user_input_tool(), + create_request_user_input_tool(false), create_apply_patch_freeform_tool(), ToolSpec::WebSearch { external_web_access: Some(true), @@ -2080,8 +2082,6 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Collab); - features.enable(Feature::CollaborationModes); - features.enable(Feature::Sqlite); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2108,7 +2108,6 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Collab); - features.enable(Feature::CollaborationModes); features.enable(Feature::Sqlite); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -2328,8 +2327,7 @@ mod tests { let config = test_config(); let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2352,8 +2350,7 @@ mod tests { let config = test_config(); let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2374,8 +2371,7 @@ mod tests { #[test] fn test_build_specs_gpt5_codex_default() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5-codex", &features, @@ -2393,8 +2389,7 @@ mod tests { #[test] fn test_build_specs_gpt51_codex_default() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex", &features, @@ -2414,7 +2409,6 @@ mod tests { fn test_build_specs_gpt5_codex_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); assert_model_tools( "gpt-5-codex", &features, @@ -2435,7 +2429,6 @@ mod tests { fn test_build_specs_gpt51_codex_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); assert_model_tools( "gpt-5.1-codex", &features, @@ -2454,8 +2447,7 @@ mod tests { #[test] fn test_gpt_5_1_codex_max_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex-max", &features, @@ -2473,8 +2465,7 @@ mod tests { #[test] fn test_codex_5_1_mini_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex-mini", &features, @@ -2492,8 +2483,7 @@ mod tests { #[test] fn test_gpt_5_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5", &features, @@ -2510,8 +2500,7 @@ mod tests { #[test] fn test_gpt_5_1_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1", &features, @@ -2531,7 +2520,6 @@ mod tests { fn test_gpt_5_1_codex_max_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); assert_model_tools( "gpt-5.1-codex-max", &features, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 5fc94a033e..8dcabc428e 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -584,6 +584,9 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { auth_manager, SessionSource::Exec, config.model_catalog.clone(), + config + .features + .enabled(Feature::DefaultModeRequestUserInput), ); let NewThread { thread: codex, .. } = thread_manager .start_thread(config) diff --git a/codex-rs/core/tests/suite/model_info_overrides.rs b/codex-rs/core/tests/suite/model_info_overrides.rs index 7a92e99f1e..4fb12f18be 100644 --- a/codex-rs/core/tests/suite/model_info_overrides.rs +++ b/codex-rs/core/tests/suite/model_info_overrides.rs @@ -12,7 +12,7 @@ async fn offline_model_info_without_tool_output_override() { let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); - let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None); + let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None, false); let model_info = manager.get_model_info("gpt-5.1", &config).await; @@ -30,7 +30,7 @@ async fn offline_model_info_with_tool_output_override() { let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); - let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None); + let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None, false); let model_info = manager.get_model_info("gpt-5.1-codex", &config).await; diff --git a/codex-rs/core/tests/suite/model_tools.rs b/codex-rs/core/tests/suite/model_tools.rs index 026be98dab..9830a78c4c 100644 --- a/codex-rs/core/tests/suite/model_tools.rs +++ b/codex-rs/core/tests/suite/model_tools.rs @@ -1,6 +1,5 @@ #![allow(clippy::unwrap_used)] -use codex_core::features::Feature; use codex_protocol::config_types::WebSearchMode; use core_test_support::responses; use core_test_support::responses::start_mock_server; @@ -40,7 +39,6 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec { .web_search_mode .set(WebSearchMode::Cached) .expect("test web_search_mode should satisfy constraints"); - config.features.enable(Feature::CollaborationModes); }); let test = builder .build(&server) diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index 88ee54b5e9..64f2e0b6db 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -86,8 +86,10 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul session_configured, .. } = builder - .with_config(|config| { - config.features.enable(Feature::CollaborationModes); + .with_config(move |config| { + if mode == ModeKind::Default { + config.features.enable(Feature::DefaultModeRequestUserInput); + } }) .build(&server) .await?; @@ -198,18 +200,13 @@ where let server = start_mock_server().await; - let builder = test_codex(); + let mut builder = test_codex(); let TestCodex { codex, cwd, session_configured, .. - } = builder - .with_config(|config| { - config.features.enable(Feature::CollaborationModes); - }) - .build(&server) - .await?; + } = builder.build(&server).await?; let mode_slug = mode_name.to_lowercase().replace(' ', "-"); let call_id = format!("user-input-{mode_slug}-call"); @@ -290,7 +287,20 @@ async fn request_user_input_rejected_in_execute_mode_alias() -> anyhow::Result<( } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn request_user_input_round_trip_in_default_mode() -> anyhow::Result<()> { +async fn request_user_input_rejected_in_default_mode_by_default() -> anyhow::Result<()> { + assert_request_user_input_rejected("Default", |model| CollaborationMode { + mode: ModeKind::Default, + settings: Settings { + model, + reasoning_effort: None, + developer_instructions: None, + }, + }) + .await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn request_user_input_round_trip_in_default_mode_with_feature() -> anyhow::Result<()> { request_user_input_round_trip_for_mode(ModeKind::Default).await } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 382081dd3d..be76728348 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -396,6 +396,9 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any auth_manager.clone(), SessionSource::Exec, config.model_catalog.clone(), + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), )); let default_model = thread_manager .get_models_manager() diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 95d48aa822..40794d2ffc 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -62,6 +62,9 @@ impl MessageProcessor { auth_manager, SessionSource::Mcp, config.model_catalog.clone(), + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), )); Self { outgoing, diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 2b2d983e4f..7586212adb 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -210,7 +210,7 @@ impl ModeKind { } pub const fn allows_request_user_input(self) -> bool { - matches!(self, Self::Plan | Self::Default) + matches!(self, Self::Plan) } } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 62002c55c4..bd136d4183 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1247,6 +1247,9 @@ impl App { auth_manager.clone(), SessionSource::Cli, config.model_catalog.clone(), + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), )); let mut model = thread_manager .get_models_manager() diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 5fa9854138..7e98836b39 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1633,7 +1633,12 @@ async fn make_chatwidget_manual( let auth_manager = codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let codex_home = cfg.codex_home.clone(); - let models_manager = Arc::new(ModelsManager::new(codex_home, auth_manager.clone(), None)); + let models_manager = Arc::new(ModelsManager::new( + codex_home, + auth_manager.clone(), + None, + false, + )); let reasoning_effort = None; let base_mode = CollaborationMode { mode: ModeKind::Default, @@ -1759,6 +1764,7 @@ fn set_chatgpt_auth(chat: &mut ChatWidget) { chat.config.codex_home.clone(), chat.auth_manager.clone(), None, + false, )); }