From 473bf8b8f514ea1582777de8457721b690bcd4bb Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 11:57:41 -0800 Subject: [PATCH 1/9] Enable request_user_input in default mode --- codex-rs/app-server/tests/suite/v2/turn_start.rs | 2 +- .../core/src/tools/handlers/request_user_input.rs | 10 +++++----- .../core/templates/collaboration_mode/default.md | 2 +- codex-rs/core/tests/suite/request_user_input.rs | 12 ++---------- codex-rs/protocol/src/config_types.rs | 2 +- 5 files changed, 10 insertions(+), 18 deletions(-) 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 eaa017d6ae80..d7e098c2f8e7 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -412,7 +412,7 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { let payload = request.body_json(); assert_eq!(payload["model"].as_str(), Some("mock-model-collab")); let payload_text = payload.to_string(); - assert!(payload_text.contains("The `request_user_input` tool is unavailable in Default mode.")); + assert!(payload_text.contains("The `request_user_input` tool is available in Default mode.")); Ok(()) } 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 134294d4287f..06ce5d6f7852 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input.rs @@ -117,9 +117,9 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn request_user_input_mode_availability_is_plan_only() { + fn request_user_input_mode_availability_includes_default_mode() { 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()); } @@ -129,7 +129,7 @@ mod tests { assert_eq!(request_user_input_unavailable_message(ModeKind::Plan), None); assert_eq!( request_user_input_unavailable_message(ModeKind::Default), - Some("request_user_input is unavailable in Default mode".to_string()) + None ); assert_eq!( request_user_input_unavailable_message(ModeKind::Execute), @@ -142,10 +142,10 @@ mod tests { } #[test] - fn request_user_input_tool_description_mentions_plan_only() { + fn request_user_input_tool_description_mentions_default_and_plan() { assert_eq!( request_user_input_tool_description(), - "Request user input for one to three short questions and wait for the response. This tool is only available in Plan mode.".to_string() + "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/templates/collaboration_mode/default.md b/codex-rs/core/templates/collaboration_mode/default.md index 4efd963ba0c3..3206387a6a0e 100644 --- a/codex-rs/core/templates/collaboration_mode/default.md +++ b/codex-rs/core/templates/collaboration_mode/default.md @@ -8,4 +8,4 @@ Your active mode changes only when new developer instructions with a different ` {{REQUEST_USER_INPUT_AVAILABILITY}} -If a decision is necessary and cannot be discovered from local context, ask the user directly. However, in Default mode you should strongly prefer executing the user's request rather than stopping to ask questions. +In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `request_user_input` tool rather than writing a multiple choice question as a textual assistant message. Never write a multiple choice question as a textual assistant message. diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index ac6c1d2f3f97..88ee54b5e947 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -290,16 +290,8 @@ 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_rejected_in_default_mode() -> anyhow::Result<()> { - assert_request_user_input_rejected("Default", |model| CollaborationMode { - mode: ModeKind::Default, - settings: Settings { - model, - reasoning_effort: None, - developer_instructions: None, - }, - }) - .await +async fn request_user_input_round_trip_in_default_mode() -> anyhow::Result<()> { + request_user_input_round_trip_for_mode(ModeKind::Default).await } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 7586212adb51..2b2d983e4f42 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) + matches!(self, Self::Plan | Self::Default) } } From 0fff60e88ba5317f9e3a524edfedcd04ebdebd47 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 14:18:46 -0800 Subject: [PATCH 2/9] 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 18d6a121e729..39a8719dc09d 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 4ee76bdca2db..926cb9bbc64c 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 d7e098c2f8e7..48d5157c14cb 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 397318c8e8fd..2a8a956ade3f 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 d3bace851940..ed1734185be6 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 7b900609b380..3eed6827e57b 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 4b1da3e58ad3..fd9e1a017fba 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 5e6d611adca2..3084d92fa872 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 cb7c464362a5..8db3bd371f0b 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 06ce5d6f7852..f9b444186edc 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::Plan, false), + None + ); + assert_eq!( + 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), + request_user_input_unavailable_message(ModeKind::Default, true), None ); assert_eq!( - request_user_input_unavailable_message(ModeKind::Execute), + 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(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(), + 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 3c9ab4bf3a8e..7c4b84b4c973 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 5fc94a033ec6..8dcabc428e6f 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 7a92e99f1ee8..4fb12f18be13 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 026be98dab83..9830a78c4ca9 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 88ee54b5e947..64f2e0b6db60 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 382081dd3de3..be767283482b 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 95d48aa8221b..40794d2ffc31 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 2b2d983e4f42..7586212adb51 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 62002c55c416..bd136d4183a8 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 5fa9854138c4..7e98836b3933 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, )); } From 558bd518476743cfb6a9374feddd3d0e6b0e98b0 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 14:26:04 -0800 Subject: [PATCH 3/9] Condition default-mode question strategy on feature flag --- .../collaboration_mode_presets.rs | 27 +++++++++++++++++++ .../templates/collaboration_mode/default.md | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) 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 3eed6827e57b..3c8999d59508 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -8,6 +8,7 @@ const COLLABORATION_MODE_DEFAULT: &str = include_str!("../../templates/collaboration_mode/default.md"); const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; +const QUESTION_STRATEGY_PLACEHOLDER: &str = "{{QUESTION_STRATEGY}}"; pub(crate) fn builtin_collaboration_mode_presets( default_mode_request_user_input: bool, @@ -44,12 +45,14 @@ 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, default_mode_request_user_input); + let question_strategy = question_strategy_message(default_mode_request_user_input); COLLABORATION_MODE_DEFAULT .replace(KNOWN_MODE_NAMES_PLACEHOLDER, &known_mode_names) .replace( REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER, &request_user_input_availability, ) + .replace(QUESTION_STRATEGY_PLACEHOLDER, &question_strategy) } fn format_mode_names(modes: &[ModeKind]) -> String { @@ -78,6 +81,14 @@ fn request_user_input_availability_message( } } +fn question_strategy_message(default_mode_request_user_input: bool) -> String { + if default_mode_request_user_input { + "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `request_user_input` tool rather than writing a multiple choice question as a textual assistant message. Never write a multiple choice question as a textual assistant message.".to_string() + } else { + "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.".to_string() + } +} + #[cfg(test)] mod tests { use super::*; @@ -102,6 +113,7 @@ mod tests { assert!(!default_instructions.contains(KNOWN_MODE_NAMES_PLACEHOLDER)); assert!(!default_instructions.contains(REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER)); + assert!(!default_instructions.contains(QUESTION_STRATEGY_PLACEHOLDER)); let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); let expected_snippet = format!("Known mode names are {known_mode_names}."); @@ -110,5 +122,20 @@ mod tests { let expected_availability_message = request_user_input_availability_message(ModeKind::Default, true); assert!(default_instructions.contains(&expected_availability_message)); + assert!(default_instructions.contains("prefer using the `request_user_input` tool")); + } + + #[test] + fn default_mode_instructions_use_plain_text_questions_when_feature_disabled() { + let default_instructions = default_preset(false) + .developer_instructions + .expect("default preset should include instructions") + .expect("default instructions should be set"); + + assert!(!default_instructions.contains("prefer using the `request_user_input` tool")); + assert!( + default_instructions + .contains("ask the user directly with a concise plain-text question") + ); } } diff --git a/codex-rs/core/templates/collaboration_mode/default.md b/codex-rs/core/templates/collaboration_mode/default.md index 3206387a6a0e..6ff434a7565d 100644 --- a/codex-rs/core/templates/collaboration_mode/default.md +++ b/codex-rs/core/templates/collaboration_mode/default.md @@ -8,4 +8,4 @@ Your active mode changes only when new developer instructions with a different ` {{REQUEST_USER_INPUT_AVAILABILITY}} -In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `request_user_input` tool rather than writing a multiple choice question as a textual assistant message. Never write a multiple choice question as a textual assistant message. +{{QUESTION_STRATEGY}} From ee475fc3f099d201f60c00c81241f74d4717911f Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 15:45:10 -0800 Subject: [PATCH 4/9] Use thread config for collaboration mode preset availability --- .../app-server/src/codex_message_processor.rs | 13 ++- .../app-server/tests/suite/v2/turn_start.rs | 87 +++++++++++++++++++ codex-rs/core/src/models_manager/manager.rs | 11 ++- 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 2d18ff7a5109..7eb2b1159f56 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -449,11 +449,15 @@ impl CodexMessageProcessor { fn normalize_turn_start_collaboration_mode( &self, mut collaboration_mode: CollaborationMode, + default_mode_request_user_input: bool, ) -> CollaborationMode { if collaboration_mode.settings.developer_instructions.is_none() && let Some(instructions) = self .thread_manager - .list_collaboration_modes() + .get_models_manager() + .list_collaboration_modes_for_default_mode_request_user_input( + default_mode_request_user_input, + ) .into_iter() .find(|preset| preset.mode == Some(collaboration_mode.mode)) .and_then(|preset| preset.developer_instructions.flatten()) @@ -5350,9 +5354,10 @@ impl CodexMessageProcessor { } }; - let collaboration_mode = params - .collaboration_mode - .map(|mode| self.normalize_turn_start_collaboration_mode(mode)); + let default_mode_request_user_input = thread.enabled(Feature::DefaultModeRequestUserInput); + let collaboration_mode = params.collaboration_mode.map(|mode| { + self.normalize_turn_start_collaboration_mode(mode, default_mode_request_user_input) + }); // Map v2 input items to core input items. let mapped_items: Vec = params 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 48d5157c14cb..afd116be1748 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -48,7 +48,9 @@ use codex_protocol::openai_models::ReasoningEffort; use core_test_support::responses; use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; +use serde_json::json; use std::collections::BTreeMap; +use std::collections::HashMap; use std::path::Path; use tempfile::TempDir; use tokio::time::timeout; @@ -417,6 +419,91 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { Ok(()) } +#[tokio::test] +async fn turn_start_uses_thread_feature_overrides_for_collaboration_mode_instructions_v2() +-> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let body = responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-1"), + ]); + let response_mock = responses::mount_sse_once(&server, body).await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + "never", + &BTreeMap::default(), + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("gpt-5.2-codex".to_string()), + config: Some(HashMap::from([( + "features.default_mode_request_user_input".to_string(), + json!(true), + )])), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let collaboration_mode = CollaborationMode { + mode: ModeKind::Default, + settings: Settings { + model: "mock-model-collab".to_string(), + reasoning_effort: Some(ReasoningEffort::High), + developer_instructions: None, + }, + }; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: vec![V2UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + model: Some("mock-model-override".to_string()), + effort: Some(ReasoningEffort::Low), + summary: Some(ReasoningSummary::Auto), + output_schema: None, + collaboration_mode: Some(collaboration_mode), + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let _turn: TurnStartResponse = to_response::(turn_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let request = response_mock.single_request(); + let payload_text = request.body_json().to_string(); + assert!(payload_text.contains("The `request_user_input` tool is available in Default mode.")); + + Ok(()) +} + #[tokio::test] async fn turn_start_accepts_personality_override_v2() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index fd9e1a017fba..3a297b32d3ae 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -100,7 +100,16 @@ impl ModelsManager { /// /// Returns a static set of presets seeded with the configured model. pub fn list_collaboration_modes(&self) -> Vec { - builtin_collaboration_mode_presets(self.default_mode_request_user_input) + self.list_collaboration_modes_for_default_mode_request_user_input( + self.default_mode_request_user_input, + ) + } + + pub fn list_collaboration_modes_for_default_mode_request_user_input( + &self, + default_mode_request_user_input: bool, + ) -> Vec { + builtin_collaboration_mode_presets(default_mode_request_user_input) } /// Attempt to list models without blocking, using the current cached state. From f1591f67efadeb3d18c852e4f0c3641f7d2a4ea5 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 15:49:55 -0800 Subject: [PATCH 5/9] Update request_user_input tools test and config schema fixture --- codex-rs/core/config.schema.json | 6 ++++++ codex-rs/core/src/tools/spec.rs | 18 +++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index ca7fb51579d4..b890748d64ad 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -334,6 +334,9 @@ "connectors": { "type": "boolean" }, + "default_mode_request_user_input": { + "type": "boolean" + }, "elevated_windows_sandbox": { "type": "boolean" }, @@ -1627,6 +1630,9 @@ "connectors": { "type": "boolean" }, + "default_mode_request_user_input": { + "type": "boolean" + }, "elevated_windows_sandbox": { "type": "boolean" }, diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 7c4b84b4c973..08a2437402a9 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -2133,12 +2133,11 @@ mod tests { } #[test] - fn request_user_input_requires_collaboration_modes_feature() { + fn request_user_input_description_reflects_default_mode_feature_flag() { 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.disable(Feature::CollaborationModes); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2146,12 +2145,13 @@ mod tests { session_source: SessionSource::Cli, }); let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); - assert!( - !tools.iter().any(|t| t.spec.name() == "request_user_input"), - "request_user_input should be disabled when collaboration_modes feature is off" + let request_user_input_tool = find_tool(&tools, "request_user_input"); + assert_eq!( + request_user_input_tool.spec, + create_request_user_input_tool(false) ); - features.enable(Feature::CollaborationModes); + features.enable(Feature::DefaultModeRequestUserInput); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2159,7 +2159,11 @@ mod tests { session_source: SessionSource::Cli, }); let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); - assert_contains_tool_names(&tools, &["request_user_input"]); + let request_user_input_tool = find_tool(&tools, "request_user_input"); + assert_eq!( + request_user_input_tool.spec, + create_request_user_input_tool(true) + ); } #[test] From ec21465e1fd7866b3af1463d79539266151ac1ca Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 16:40:25 -0800 Subject: [PATCH 6/9] Refactor collaboration mode preset availability config --- .../app-server/src/codex_message_processor.rs | 12 ++--- codex-rs/app-server/src/message_processor.rs | 9 ++-- codex-rs/core/src/codex.rs | 8 ++-- .../collaboration_mode_presets.rs | 46 ++++++++++++------- codex-rs/core/src/models_manager/manager.rs | 45 +++++++++++------- codex-rs/core/src/test_support.rs | 4 +- codex-rs/core/src/thread_manager.rs | 5 +- codex-rs/core/tests/suite/client.rs | 9 ++-- .../core/tests/suite/model_info_overrides.rs | 15 +++++- codex-rs/exec/src/lib.rs | 9 ++-- codex-rs/mcp-server/src/message_processor.rs | 9 ++-- codex-rs/tui/src/app.rs | 9 ++-- codex-rs/tui/src/chatwidget/tests.rs | 5 +- 13 files changed, 121 insertions(+), 64 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 7eb2b1159f56..14748fa4b99d 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -208,6 +208,7 @@ use codex_core::find_thread_path_by_id_str; use codex_core::git_info::git_diff_to_remote; use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::parse_cursor; use codex_core::read_head_for_summary; use codex_core::read_session_meta_line; @@ -449,15 +450,13 @@ impl CodexMessageProcessor { fn normalize_turn_start_collaboration_mode( &self, mut collaboration_mode: CollaborationMode, - default_mode_request_user_input: bool, + collaboration_modes_config: CollaborationModesConfig, ) -> CollaborationMode { if collaboration_mode.settings.developer_instructions.is_none() && let Some(instructions) = self .thread_manager .get_models_manager() - .list_collaboration_modes_for_default_mode_request_user_input( - default_mode_request_user_input, - ) + .list_collaboration_modes_for_config(collaboration_modes_config) .into_iter() .find(|preset| preset.mode == Some(collaboration_mode.mode)) .and_then(|preset| preset.developer_instructions.flatten()) @@ -5354,9 +5353,10 @@ impl CodexMessageProcessor { } }; - let default_mode_request_user_input = thread.enabled(Feature::DefaultModeRequestUserInput); + let collaboration_modes_config = + CollaborationModesConfig::new(thread.enabled(Feature::DefaultModeRequestUserInput)); let collaboration_mode = params.collaboration_mode.map(|mode| { - self.normalize_turn_start_collaboration_mode(mode, default_mode_request_user_input) + self.normalize_turn_start_collaboration_mode(mode, collaboration_modes_config) }); // Map v2 input items to core input items. diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 39a8719dc09d..59c9690f5016 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -46,6 +46,7 @@ use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; use codex_core::default_client::set_default_client_residency_requirement; use codex_core::default_client::set_default_originator; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_feedback::CodexFeedback; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; @@ -177,9 +178,11 @@ impl MessageProcessor { auth_manager.clone(), SessionSource::VSCode, config.model_catalog.clone(), - config - .features - .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + CollaborationModesConfig::new( + 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/core/src/codex.rs b/codex-rs/core/src/codex.rs index 2a8a956ade3f..e229d37b5bd9 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -29,6 +29,8 @@ use crate::features::FEATURES; use crate::features::Feature; use crate::features::Features; use crate::features::maybe_push_unstable_features_warning; +#[cfg(test)] +use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::models_manager::manager::ModelsManager; use crate::parse_command::parse_command; use crate::parse_turn_item; @@ -8059,7 +8061,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, - false, + CollaborationModesConfig::default(), )); let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); let model_info = @@ -8135,7 +8137,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, - false, + CollaborationModesConfig::default(), )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); @@ -8291,7 +8293,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, - false, + CollaborationModesConfig::default(), )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); 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 3c8999d59508..7b0a9b22d46c 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -10,13 +10,23 @@ const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; const QUESTION_STRATEGY_PLACEHOLDER: &str = "{{QUESTION_STRATEGY}}"; +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct CollaborationModesConfig { + pub default_mode_request_user_input: bool, +} + +impl CollaborationModesConfig { + pub const fn new(default_mode_request_user_input: bool) -> Self { + Self { + default_mode_request_user_input, + } + } +} + pub(crate) fn builtin_collaboration_mode_presets( - default_mode_request_user_input: bool, + collaboration_modes_config: CollaborationModesConfig, ) -> Vec { - vec![ - plan_preset(), - default_preset(default_mode_request_user_input), - ] + vec![plan_preset(), default_preset(collaboration_modes_config)] } fn plan_preset() -> CollaborationModeMask { @@ -29,23 +39,24 @@ fn plan_preset() -> CollaborationModeMask { } } -fn default_preset(default_mode_request_user_input: bool) -> CollaborationModeMask { +fn default_preset(collaboration_modes_config: CollaborationModesConfig) -> 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( - default_mode_request_user_input, - ))), + developer_instructions: Some(Some(default_mode_instructions(collaboration_modes_config))), } } -fn default_mode_instructions(default_mode_request_user_input: bool) -> String { +fn default_mode_instructions(collaboration_modes_config: CollaborationModesConfig) -> String { let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); - let request_user_input_availability = - request_user_input_availability_message(ModeKind::Default, default_mode_request_user_input); - let question_strategy = question_strategy_message(default_mode_request_user_input); + let request_user_input_availability = request_user_input_availability_message( + ModeKind::Default, + collaboration_modes_config.default_mode_request_user_input, + ); + let question_strategy = + question_strategy_message(collaboration_modes_config.default_mode_request_user_input); COLLABORATION_MODE_DEFAULT .replace(KNOWN_MODE_NAMES_PLACEHOLDER, &known_mode_names) .replace( @@ -97,7 +108,10 @@ mod tests { #[test] fn preset_names_use_mode_display_names() { assert_eq!(plan_preset().name, ModeKind::Plan.display_name()); - assert_eq!(default_preset(false).name, ModeKind::Default.display_name()); + assert_eq!( + default_preset(CollaborationModesConfig::default()).name, + ModeKind::Default.display_name() + ); assert_eq!( plan_preset().reasoning_effort, Some(Some(ReasoningEffort::Medium)) @@ -106,7 +120,7 @@ mod tests { #[test] fn default_mode_instructions_replace_mode_names_placeholder() { - let default_instructions = default_preset(true) + let default_instructions = default_preset(CollaborationModesConfig::new(true)) .developer_instructions .expect("default preset should include instructions") .expect("default instructions should be set"); @@ -127,7 +141,7 @@ mod tests { #[test] fn default_mode_instructions_use_plain_text_questions_when_feature_disabled() { - let default_instructions = default_preset(false) + let default_instructions = default_preset(CollaborationModesConfig::default()) .developer_instructions .expect("default preset should include instructions") .expect("default instructions should be set"); diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index 3a297b32d3ae..bb3157619f6e 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -8,6 +8,7 @@ use crate::default_client::build_reqwest_client; use crate::error::CodexErr; use crate::error::Result as CoreResult; use crate::model_provider_info::ModelProviderInfo; +use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets; use crate::models_manager::model_info; use codex_api::ModelsClient; @@ -46,7 +47,7 @@ pub enum RefreshStrategy { pub struct ModelsManager { remote_models: RwLock>, has_custom_model_catalog: bool, - default_mode_request_user_input: bool, + collaboration_modes_config: CollaborationModesConfig, auth_manager: Arc, etag: RwLock>, cache_manager: ModelsCacheManager, @@ -63,7 +64,7 @@ impl ModelsManager { codex_home: PathBuf, auth_manager: Arc, model_catalog: Option, - default_mode_request_user_input: bool, + collaboration_modes_config: CollaborationModesConfig, ) -> Self { let cache_path = codex_home.join(MODEL_CACHE_FILE); let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL); @@ -77,7 +78,7 @@ impl ModelsManager { Self { remote_models: RwLock::new(remote_models), has_custom_model_catalog, - default_mode_request_user_input, + collaboration_modes_config, auth_manager, etag: RwLock::new(None), cache_manager, @@ -100,16 +101,14 @@ impl ModelsManager { /// /// Returns a static set of presets seeded with the configured model. pub fn list_collaboration_modes(&self) -> Vec { - self.list_collaboration_modes_for_default_mode_request_user_input( - self.default_mode_request_user_input, - ) + self.list_collaboration_modes_for_config(self.collaboration_modes_config) } - pub fn list_collaboration_modes_for_default_mode_request_user_input( + pub fn list_collaboration_modes_for_config( &self, - default_mode_request_user_input: bool, + collaboration_modes_config: CollaborationModesConfig, ) -> Vec { - builtin_collaboration_mode_presets(default_mode_request_user_input) + builtin_collaboration_mode_presets(collaboration_modes_config) } /// Attempt to list models without blocking, using the current cached state. @@ -377,7 +376,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, + collaboration_modes_config: CollaborationModesConfig::default(), auth_manager, etag: RwLock::new(None), cache_manager, @@ -504,8 +503,12 @@ 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, false); + let manager = ModelsManager::new( + codex_home.path().to_path_buf(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let known_slug = manager .get_remote_models() .await @@ -542,7 +545,7 @@ mod tests { Some(ModelsResponse { models: vec![remote_model("gpt-overlay", "Overlay", 0)], }), - false, + CollaborationModesConfig::default(), ); let model_info = manager @@ -566,8 +569,12 @@ 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, false); + let manager = ModelsManager::new( + codex_home.path().to_path_buf(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let known_slug = manager .get_remote_models() .await @@ -593,8 +600,12 @@ 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, false); + let manager = ModelsManager::new( + codex_home.path().to_path_buf(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); 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 3084d92fa872..12ba7cda829b 100644 --- a/codex-rs/core/src/test_support.rs +++ b/codex-rs/core/src/test_support.rs @@ -85,5 +85,7 @@ pub fn all_model_presets() -> &'static Vec { } pub fn builtin_collaboration_mode_presets() -> Vec { - collaboration_mode_presets::builtin_collaboration_mode_presets(false) + collaboration_mode_presets::builtin_collaboration_mode_presets( + collaboration_mode_presets::CollaborationModesConfig::default(), + ) } diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 8db3bd371f0b..dbddc622a0b9 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -11,6 +11,7 @@ use crate::error::CodexErr; use crate::error::Result as CodexResult; use crate::file_watcher::FileWatcher; use crate::file_watcher::FileWatcherEvent; +use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::models_manager::manager::ModelsManager; use crate::protocol::Event; use crate::protocol::EventMsg; @@ -143,7 +144,7 @@ impl ThreadManager { auth_manager: Arc, session_source: SessionSource, model_catalog: Option, - default_mode_request_user_input: bool, + collaboration_modes_config: CollaborationModesConfig, ) -> Self { let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY); let skills_manager = Arc::new(SkillsManager::new(codex_home.clone())); @@ -156,7 +157,7 @@ impl ThreadManager { codex_home, auth_manager.clone(), model_catalog, - default_mode_request_user_input, + collaboration_modes_config, )), skills_manager, file_watcher, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 8dcabc428e6f..f61a3a33f253 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -12,6 +12,7 @@ use codex_core::built_in_model_providers; use codex_core::default_client::originator; use codex_core::error::CodexErr; use codex_core::features::Feature; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_otel::OtelManager; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; @@ -584,9 +585,11 @@ 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), + CollaborationModesConfig::new( + 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 4fb12f18be13..ee4afc36bf24 100644 --- a/codex-rs/core/tests/suite/model_info_overrides.rs +++ b/codex-rs/core/tests/suite/model_info_overrides.rs @@ -1,4 +1,5 @@ use codex_core::CodexAuth; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::ModelsManager; use codex_protocol::openai_models::TruncationPolicyConfig; use core_test_support::load_default_config_for_test; @@ -12,7 +13,12 @@ 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, false); + let manager = ModelsManager::new( + config.codex_home.clone(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let model_info = manager.get_model_info("gpt-5.1", &config).await; @@ -30,7 +36,12 @@ 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, false); + let manager = ModelsManager::new( + config.codex_home.clone(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let model_info = manager.get_model_info("gpt-5.1-codex", &config).await; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index be767283482b..f99e8d2d14f7 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -31,6 +31,7 @@ use codex_core::config_loader::ConfigLoadError; use codex_core::config_loader::format_config_error_with_source; use codex_core::format_exec_policy_error_with_source; use codex_core::git_info::get_git_repo_root; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::RefreshStrategy; use codex_protocol::approvals::ElicitationAction; use codex_protocol::config_types::SandboxMode; @@ -396,9 +397,11 @@ 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), + CollaborationModesConfig::new( + 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 40794d2ffc31..624deb3ff640 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -6,6 +6,7 @@ use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::Submission; @@ -62,9 +63,11 @@ impl MessageProcessor { auth_manager, SessionSource::Mcp, config.model_catalog.clone(), - config - .features - .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + CollaborationModesConfig::new( + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + ), )); Self { outgoing, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index bd136d4183a8..32ddce6b125e 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -47,6 +47,7 @@ use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config_loader::ConfigLayerStackOrdering; use codex_core::features::Feature; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::RefreshStrategy; use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG; use codex_core::models_manager::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG; @@ -1247,9 +1248,11 @@ impl App { auth_manager.clone(), SessionSource::Cli, config.model_catalog.clone(), - config - .features - .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + CollaborationModesConfig::new( + 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 7e98836b3933..8f86662dd631 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -24,6 +24,7 @@ use codex_core::config::ConstraintError; use codex_core::config::types::WindowsSandboxModeToml; use codex_core::config_loader::RequirementSource; use codex_core::features::Feature; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::ModelsManager; use codex_core::skills::model::SkillMetadata; use codex_core::terminal::TerminalName; @@ -1637,7 +1638,7 @@ async fn make_chatwidget_manual( codex_home, auth_manager.clone(), None, - false, + CollaborationModesConfig::default(), )); let reasoning_effort = None; let base_mode = CollaborationMode { @@ -1764,7 +1765,7 @@ fn set_chatgpt_auth(chat: &mut ChatWidget) { chat.config.codex_home.clone(), chat.auth_manager.clone(), None, - false, + CollaborationModesConfig::default(), )); } From 9f9475dd669c262c1681e93a23d31e31a14b15f8 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 17:05:46 -0800 Subject: [PATCH 7/9] Document collaboration mode feature-flag config --- .../core/src/models_manager/collaboration_mode_presets.rs | 6 ++++++ 1 file changed, 6 insertions(+) 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 7b0a9b22d46c..23ea1b22789c 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -10,8 +10,14 @@ const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; const QUESTION_STRATEGY_PLACEHOLDER: &str = "{{QUESTION_STRATEGY}}"; +/// Stores feature flags that control collaboration-mode behavior. +/// +/// Keep mode-related flags here so new collaboration-mode capabilities can be +/// added without large cross-cutting diffs to constructor and call-site +/// signatures. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct CollaborationModesConfig { + /// Enables `request_user_input` availability in Default mode. pub default_mode_request_user_input: bool, } From 5bd19a2e078cd4c1cbec213ade77c9ce8a17c0da Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 17:23:51 -0800 Subject: [PATCH 8/9] Rename asking guidance placeholder in default mode template --- .../models_manager/collaboration_mode_presets.rs | 16 ++++++++++------ .../core/templates/collaboration_mode/default.md | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) 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 23ea1b22789c..a9a7321714a7 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -8,7 +8,7 @@ const COLLABORATION_MODE_DEFAULT: &str = include_str!("../../templates/collaboration_mode/default.md"); const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; -const QUESTION_STRATEGY_PLACEHOLDER: &str = "{{QUESTION_STRATEGY}}"; +const ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER: &str = "{{ASKING_QUESTIONS_GUIDANCE}}"; /// Stores feature flags that control collaboration-mode behavior. /// @@ -61,15 +61,19 @@ fn default_mode_instructions(collaboration_modes_config: CollaborationModesConfi ModeKind::Default, collaboration_modes_config.default_mode_request_user_input, ); - let question_strategy = - question_strategy_message(collaboration_modes_config.default_mode_request_user_input); + let asking_questions_guidance = asking_questions_guidance_message( + collaboration_modes_config.default_mode_request_user_input, + ); COLLABORATION_MODE_DEFAULT .replace(KNOWN_MODE_NAMES_PLACEHOLDER, &known_mode_names) .replace( REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER, &request_user_input_availability, ) - .replace(QUESTION_STRATEGY_PLACEHOLDER, &question_strategy) + .replace( + ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER, + &asking_questions_guidance, + ) } fn format_mode_names(modes: &[ModeKind]) -> String { @@ -98,7 +102,7 @@ fn request_user_input_availability_message( } } -fn question_strategy_message(default_mode_request_user_input: bool) -> String { +fn asking_questions_guidance_message(default_mode_request_user_input: bool) -> String { if default_mode_request_user_input { "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `request_user_input` tool rather than writing a multiple choice question as a textual assistant message. Never write a multiple choice question as a textual assistant message.".to_string() } else { @@ -133,7 +137,7 @@ mod tests { assert!(!default_instructions.contains(KNOWN_MODE_NAMES_PLACEHOLDER)); assert!(!default_instructions.contains(REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER)); - assert!(!default_instructions.contains(QUESTION_STRATEGY_PLACEHOLDER)); + assert!(!default_instructions.contains(ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER)); let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); let expected_snippet = format!("Known mode names are {known_mode_names}."); diff --git a/codex-rs/core/templates/collaboration_mode/default.md b/codex-rs/core/templates/collaboration_mode/default.md index 6ff434a7565d..ff00857c6d16 100644 --- a/codex-rs/core/templates/collaboration_mode/default.md +++ b/codex-rs/core/templates/collaboration_mode/default.md @@ -8,4 +8,4 @@ Your active mode changes only when new developer instructions with a different ` {{REQUEST_USER_INPUT_AVAILABILITY}} -{{QUESTION_STRATEGY}} +{{ASKING_QUESTIONS_GUIDANCE}} From 6bbc41882e688bc3e9e5bfa68ada6af54fde9394 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 17:39:46 -0800 Subject: [PATCH 9/9] Use struct literals for collaboration mode config --- .../app-server/src/codex_message_processor.rs | 5 +++-- codex-rs/app-server/src/message_processor.rs | 6 +++--- .../collaboration_mode_presets.rs | 18 ++++++------------ codex-rs/core/tests/suite/client.rs | 6 +++--- codex-rs/exec/src/lib.rs | 6 +++--- codex-rs/mcp-server/src/message_processor.rs | 6 +++--- codex-rs/tui/src/app.rs | 6 +++--- 7 files changed, 24 insertions(+), 29 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 14748fa4b99d..214ed553c8ca 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -5353,8 +5353,9 @@ impl CodexMessageProcessor { } }; - let collaboration_modes_config = - CollaborationModesConfig::new(thread.enabled(Feature::DefaultModeRequestUserInput)); + let collaboration_modes_config = CollaborationModesConfig { + default_mode_request_user_input: thread.enabled(Feature::DefaultModeRequestUserInput), + }; let collaboration_mode = params.collaboration_mode.map(|mode| { self.normalize_turn_start_collaboration_mode(mode, collaboration_modes_config) }); diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 59c9690f5016..79b6706ad87c 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -178,11 +178,11 @@ impl MessageProcessor { auth_manager.clone(), SessionSource::VSCode, config.model_catalog.clone(), - CollaborationModesConfig::new( - config + CollaborationModesConfig { + default_mode_request_user_input: 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/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs index a9a7321714a7..5c5b2124037d 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -21,14 +21,6 @@ pub struct CollaborationModesConfig { pub default_mode_request_user_input: bool, } -impl CollaborationModesConfig { - pub const fn new(default_mode_request_user_input: bool) -> Self { - Self { - default_mode_request_user_input, - } - } -} - pub(crate) fn builtin_collaboration_mode_presets( collaboration_modes_config: CollaborationModesConfig, ) -> Vec { @@ -130,10 +122,12 @@ mod tests { #[test] fn default_mode_instructions_replace_mode_names_placeholder() { - let default_instructions = default_preset(CollaborationModesConfig::new(true)) - .developer_instructions - .expect("default preset should include instructions") - .expect("default instructions should be set"); + let default_instructions = default_preset(CollaborationModesConfig { + default_mode_request_user_input: true, + }) + .developer_instructions + .expect("default preset should include instructions") + .expect("default instructions should be set"); assert!(!default_instructions.contains(KNOWN_MODE_NAMES_PLACEHOLDER)); assert!(!default_instructions.contains(REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER)); diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index f61a3a33f253..e040e61e77fa 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -585,11 +585,11 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { auth_manager, SessionSource::Exec, config.model_catalog.clone(), - CollaborationModesConfig::new( - config + CollaborationModesConfig { + default_mode_request_user_input: config .features .enabled(Feature::DefaultModeRequestUserInput), - ), + }, ); let NewThread { thread: codex, .. } = thread_manager .start_thread(config) diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index f99e8d2d14f7..89866f75c276 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -397,11 +397,11 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any auth_manager.clone(), SessionSource::Exec, config.model_catalog.clone(), - CollaborationModesConfig::new( - config + CollaborationModesConfig { + default_mode_request_user_input: 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 624deb3ff640..94a5fa441fe6 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -63,11 +63,11 @@ impl MessageProcessor { auth_manager, SessionSource::Mcp, config.model_catalog.clone(), - CollaborationModesConfig::new( - config + CollaborationModesConfig { + default_mode_request_user_input: config .features .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), - ), + }, )); Self { outgoing, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 32ddce6b125e..e4340a029253 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1248,11 +1248,11 @@ impl App { auth_manager.clone(), SessionSource::Cli, config.model_catalog.clone(), - CollaborationModesConfig::new( - config + CollaborationModesConfig { + default_mode_request_user_input: config .features .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), - ), + }, )); let mut model = thread_manager .get_models_manager()