From 69cfc73dc6d9cfd7cf0ad4c47942dfcaeebee2ac Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Fri, 23 Jan 2026 17:00:23 -0800 Subject: [PATCH] change collaboration mode to struct (#9793) Shouldn't cause behavioral change --- .../tests/suite/v2/collaboration_mode_list.rs | 7 +- .../tests/suite/v2/request_user_input.rs | 14 +- .../app-server/tests/suite/v2/turn_start.rs | 14 +- codex-rs/core/src/codex.rs | 80 +++++++----- .../collaboration_mode_presets.rs | 40 +++--- .../src/tools/handlers/request_user_input.rs | 8 +- codex-rs/core/tests/suite/client.rs | 14 +- .../tests/suite/collaboration_instructions.rs | 14 +- codex-rs/core/tests/suite/override_updates.rs | 14 +- codex-rs/core/tests/suite/prompt_caching.rs | 14 +- .../core/tests/suite/request_user_input.rs | 28 ++-- codex-rs/protocol/src/config_types.rs | 39 ++---- codex-rs/protocol/src/models.rs | 9 +- codex-rs/tui/src/chatwidget.rs | 75 ++++++----- codex-rs/tui/src/chatwidget/tests.rs | 122 +++++++++++------- codex-rs/tui/src/collaboration_modes.rs | 9 +- 16 files changed, 291 insertions(+), 210 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs index d75a589d00..f3a931f003 100644 --- a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs +++ b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs @@ -17,6 +17,7 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_core::models_manager::test_builtin_collaboration_mode_presets; use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; use pretty_assertions::assert_eq; use tempfile::TempDir; use tokio::time::timeout; @@ -57,7 +58,7 @@ fn plan_preset() -> CollaborationMode { let presets = test_builtin_collaboration_mode_presets(); presets .into_iter() - .find(|p| matches!(p, CollaborationMode::Plan(_))) + .find(|p| p.mode == ModeKind::Plan) .unwrap() } @@ -69,7 +70,7 @@ fn pair_programming_preset() -> CollaborationMode { let presets = test_builtin_collaboration_mode_presets(); presets .into_iter() - .find(|p| matches!(p, CollaborationMode::PairProgramming(_))) + .find(|p| p.mode == ModeKind::PairProgramming) .unwrap() } @@ -81,6 +82,6 @@ fn execute_preset() -> CollaborationMode { let presets = test_builtin_collaboration_mode_presets(); presets .into_iter() - .find(|p| matches!(p, CollaborationMode::Execute(_))) + .find(|p| p.mode == ModeKind::Execute) .unwrap() } 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 c66dd05bec..4ee76bdca2 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 @@ -13,6 +13,7 @@ use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput as V2UserInput; use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::openai_models::ReasoningEffort; use tokio::time::timeout; @@ -54,11 +55,14 @@ async fn request_user_input_round_trip() -> Result<()> { }], model: Some("mock-model".to_string()), effort: Some(ReasoningEffort::Medium), - collaboration_mode: Some(CollaborationMode::Plan(Settings { - model: "mock-model".to_string(), - reasoning_effort: Some(ReasoningEffort::Medium), - developer_instructions: None, - })), + collaboration_mode: Some(CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: "mock-model".to_string(), + reasoning_effort: Some(ReasoningEffort::Medium), + developer_instructions: None, + }, + }), ..Default::default() }) .await?; 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 bf04eaf252..d375a2db2f 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -38,6 +38,7 @@ use codex_core::features::FEATURES; use codex_core::features::Feature; use codex_core::protocol_config_types::ReasoningSummary; use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::Settings; use codex_protocol::openai_models::ReasoningEffort; @@ -363,11 +364,14 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { .await??; let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; - let collaboration_mode = CollaborationMode::Custom(Settings { - model: "mock-model-collab".to_string(), - reasoning_effort: Some(ReasoningEffort::High), - developer_instructions: None, - }); + let collaboration_mode = CollaborationMode { + mode: ModeKind::Custom, + 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 { diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index ea52aad32f..25c2b225a3 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -35,6 +35,7 @@ use async_channel::Receiver; use async_channel::Sender; use codex_protocol::ThreadId; use codex_protocol::approvals::ExecPolicyAmendment; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::config_types::WebSearchMode; use codex_protocol::items::TurnItem; @@ -295,11 +296,14 @@ impl Codex { // TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode // to avoid extracting these fields separately and constructing CollaborationMode here. - let collaboration_mode = CollaborationMode::Custom(Settings { - model: model.clone(), - reasoning_effort: config.model_reasoning_effort, - developer_instructions: None, - }); + let collaboration_mode = CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { + model: model.clone(), + reasoning_effort: config.model_reasoning_effort, + developer_instructions: None, + }, + }; let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, @@ -2215,6 +2219,7 @@ mod handlers { use crate::context_manager::is_user_turn_boundary; use codex_protocol::config_types::CollaborationMode; + use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::user_input::UserInput; use codex_rmcp_client::ElicitationAction; @@ -2291,11 +2296,14 @@ mod handlers { personality, } => { let collaboration_mode = collaboration_mode.or_else(|| { - Some(CollaborationMode::Custom(Settings { - model: model.clone(), - reasoning_effort: effort, - developer_instructions: None, - })) + Some(CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { + model: model.clone(), + reasoning_effort: effort, + developer_instructions: None, + }, + }) }); ( items, @@ -3878,11 +3886,14 @@ mod tests { let model = ModelsManager::get_model_offline(config.model.as_deref()); let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let reasoning_effort = config.model_reasoning_effort; - let collaboration_mode = CollaborationMode::Custom(Settings { - model, - reasoning_effort, - developer_instructions: None, - }); + let collaboration_mode = CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { + model, + reasoning_effort, + developer_instructions: None, + }, + }; let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, @@ -3954,11 +3965,14 @@ mod tests { let model = ModelsManager::get_model_offline(config.model.as_deref()); let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let reasoning_effort = config.model_reasoning_effort; - let collaboration_mode = CollaborationMode::Custom(Settings { - model, - reasoning_effort, - developer_instructions: None, - }); + let collaboration_mode = CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { + model, + reasoning_effort, + developer_instructions: None, + }, + }; let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, @@ -4214,11 +4228,14 @@ mod tests { let model = ModelsManager::get_model_offline(config.model.as_deref()); let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let reasoning_effort = config.model_reasoning_effort; - let collaboration_mode = CollaborationMode::Custom(Settings { - model, - reasoning_effort, - developer_instructions: None, - }); + let collaboration_mode = CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { + model, + reasoning_effort, + developer_instructions: None, + }, + }; let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, @@ -4319,11 +4336,14 @@ mod tests { let model = ModelsManager::get_model_offline(config.model.as_deref()); let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let reasoning_effort = config.model_reasoning_effort; - let collaboration_mode = CollaborationMode::Custom(Settings { - model, - reasoning_effort, - developer_instructions: None, - }); + let collaboration_mode = CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { + model, + reasoning_effort, + developer_instructions: None, + }, + }; let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, 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 39d725bcd6..70989cb7cd 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -1,4 +1,5 @@ use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::openai_models::ReasoningEffort; @@ -18,25 +19,34 @@ pub fn test_builtin_collaboration_mode_presets() -> Vec { } fn plan_preset() -> CollaborationMode { - CollaborationMode::Plan(Settings { - model: "gpt-5.2-codex".to_string(), - reasoning_effort: Some(ReasoningEffort::High), - developer_instructions: Some(COLLABORATION_MODE_PLAN.to_string()), - }) + CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: "gpt-5.2-codex".to_string(), + reasoning_effort: Some(ReasoningEffort::High), + developer_instructions: Some(COLLABORATION_MODE_PLAN.to_string()), + }, + } } fn pair_programming_preset() -> CollaborationMode { - CollaborationMode::PairProgramming(Settings { - model: "gpt-5.2-codex".to_string(), - reasoning_effort: Some(ReasoningEffort::Medium), - developer_instructions: Some(COLLABORATION_MODE_PAIR_PROGRAMMING.to_string()), - }) + CollaborationMode { + mode: ModeKind::PairProgramming, + settings: Settings { + model: "gpt-5.2-codex".to_string(), + reasoning_effort: Some(ReasoningEffort::Medium), + developer_instructions: Some(COLLABORATION_MODE_PAIR_PROGRAMMING.to_string()), + }, + } } fn execute_preset() -> CollaborationMode { - CollaborationMode::Execute(Settings { - model: "gpt-5.2-codex".to_string(), - reasoning_effort: Some(ReasoningEffort::High), - developer_instructions: Some(COLLABORATION_MODE_EXECUTE.to_string()), - }) + CollaborationMode { + mode: ModeKind::Execute, + settings: Settings { + model: "gpt-5.2-codex".to_string(), + reasoning_effort: Some(ReasoningEffort::High), + developer_instructions: Some(COLLABORATION_MODE_EXECUTE.to_string()), + }, + } } 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 aca43a1d12..d2c55b08df 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input.rs @@ -7,7 +7,7 @@ use crate::tools::context::ToolPayload; use crate::tools::handlers::parse_arguments; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; -use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; use codex_protocol::request_user_input::RequestUserInputArgs; pub struct RequestUserInputHandler; @@ -36,9 +36,9 @@ impl ToolHandler for RequestUserInputHandler { } }; - let disallowed_mode = match session.collaboration_mode().await { - CollaborationMode::Execute(_) => Some("Execute"), - CollaborationMode::Custom(_) => Some("Custom"), + let disallowed_mode = match session.collaboration_mode().await.mode { + ModeKind::Execute => Some("Execute"), + ModeKind::Custom => Some("Custom"), _ => None, }; if let Some(mode_name) = disallowed_mode { diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index eb08de075c..902609afc5 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -23,6 +23,7 @@ use codex_core::protocol::SessionSource; use codex_otel::OtelManager; use codex_protocol::ThreadId; use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Settings; use codex_protocol::config_types::Verbosity; @@ -887,11 +888,14 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re .build(&server) .await?; - let collaboration_mode = CollaborationMode::Custom(Settings { - model: "gpt-5.1".to_string(), - reasoning_effort: Some(ReasoningEffort::High), - developer_instructions: None, - }); + let collaboration_mode = CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { + model: "gpt-5.1".to_string(), + reasoning_effort: Some(ReasoningEffort::High), + developer_instructions: None, + }, + }; codex .submit(Op::UserTurn { diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 3304111698..3c6619fcea 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -4,6 +4,7 @@ use codex_core::protocol::COLLABORATION_MODE_OPEN_TAG; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_completed; @@ -22,11 +23,14 @@ fn sse_completed(id: &str) -> String { } fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMode { - CollaborationMode::Custom(Settings { - model: "gpt-5.1".to_string(), - reasoning_effort: None, - developer_instructions: instructions.map(str::to_string), - }) + CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { + model: "gpt-5.1".to_string(), + reasoning_effort: None, + developer_instructions: instructions.map(str::to_string), + }, + } } fn developer_texts(input: &[Value]) -> Vec { diff --git a/codex-rs/core/tests/suite/override_updates.rs b/codex-rs/core/tests/suite/override_updates.rs index 924fde733b..3ceb847daa 100644 --- a/codex-rs/core/tests/suite/override_updates.rs +++ b/codex-rs/core/tests/suite/override_updates.rs @@ -9,6 +9,7 @@ use codex_core::protocol::RolloutItem; use codex_core::protocol::RolloutLine; use codex_core::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; @@ -23,11 +24,14 @@ use std::time::Duration; use tempfile::TempDir; fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMode { - CollaborationMode::Custom(Settings { - model: "gpt-5.1".to_string(), - reasoning_effort: None, - developer_instructions: instructions.map(str::to_string), - }) + CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { + model: "gpt-5.1".to_string(), + reasoning_effort: None, + developer_instructions: instructions.map(str::to_string), + }, + } } fn collab_xml(text: &str) -> String { diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index c7a3eb78a8..686428b21a 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -12,6 +12,7 @@ use codex_core::protocol_config_types::ReasoningSummary; use codex_core::shell::Shell; use codex_core::shell::default_user_shell; use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::config_types::WebSearchMode; use codex_protocol::openai_models::ReasoningEffort; @@ -412,11 +413,14 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul let TestCodex { codex, .. } = test_codex().build(&server).await?; - let collaboration_mode = CollaborationMode::Custom(Settings { - model: "gpt-5.1".to_string(), - reasoning_effort: Some(ReasoningEffort::High), - developer_instructions: None, - }); + let collaboration_mode = CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { + model: "gpt-5.1".to_string(), + reasoning_effort: Some(ReasoningEffort::High), + developer_instructions: None, + }, + }; codex .submit(Op::OverrideTurnContext { diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index 59f6944577..74d3fc98f2 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -8,6 +8,7 @@ use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Settings; use codex_protocol::request_user_input::RequestUserInputAnswer; @@ -132,11 +133,14 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()> model: session_model, effort: None, summary: ReasoningSummary::Auto, - collaboration_mode: Some(CollaborationMode::Plan(Settings { - model: session_configured.model.clone(), - reasoning_effort: None, - developer_instructions: None, - })), + collaboration_mode: Some(CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: session_configured.model.clone(), + reasoning_effort: None, + developer_instructions: None, + }, + }), personality: None, }) .await?; @@ -269,24 +273,26 @@ where #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn request_user_input_rejected_in_execute_mode() -> anyhow::Result<()> { - assert_request_user_input_rejected("Execute", |model| { - CollaborationMode::Execute(Settings { + assert_request_user_input_rejected("Execute", |model| CollaborationMode { + mode: ModeKind::Execute, + settings: Settings { model, reasoning_effort: None, developer_instructions: None, - }) + }, }) .await } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn request_user_input_rejected_in_custom_mode() -> anyhow::Result<()> { - assert_request_user_input_rejected("Custom", |model| { - CollaborationMode::Custom(Settings { + assert_request_user_input_rejected("Custom", |model| CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { model, reasoning_effort: None, developer_instructions: None, - }) + }, }) .await } diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 374a9d1b11..c242d07de4 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -163,31 +163,24 @@ pub enum ModeKind { /// Collaboration mode for a Codex session. #[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)] -#[serde(tag = "mode", rename_all = "lowercase")] -pub enum CollaborationMode { - Plan(Settings), - PairProgramming(Settings), - Execute(Settings), - Custom(Settings), +#[serde(rename_all = "lowercase")] +pub struct CollaborationMode { + pub mode: ModeKind, + pub settings: Settings, } impl CollaborationMode { - /// Returns a reference to the settings, regardless of variant. - fn settings(&self) -> &Settings { - match self { - CollaborationMode::Plan(settings) - | CollaborationMode::PairProgramming(settings) - | CollaborationMode::Execute(settings) - | CollaborationMode::Custom(settings) => settings, - } + /// Returns a reference to the settings. + fn settings_ref(&self) -> &Settings { + &self.settings } pub fn model(&self) -> &str { - self.settings().model.as_str() + self.settings_ref().model.as_str() } pub fn reasoning_effort(&self) -> Option { - self.settings().reasoning_effort + self.settings_ref().reasoning_effort } /// Updates the collaboration mode with new model and/or effort values. @@ -196,14 +189,14 @@ impl CollaborationMode { /// - `effort`: `Some(Some(e))` to set effort to `e`, `Some(None)` to clear effort, `None` to keep current effort /// - `developer_instructions`: `Some(s)` to update developer instructions, `None` to keep current /// - /// Returns a new `CollaborationMode` with updated values, preserving the variant. + /// Returns a new `CollaborationMode` with updated values, preserving the mode. pub fn with_updates( &self, model: Option, effort: Option>, developer_instructions: Option, ) -> Self { - let settings = self.settings(); + let settings = self.settings_ref(); let updated_settings = Settings { model: model.unwrap_or_else(|| settings.model.clone()), reasoning_effort: effort.unwrap_or(settings.reasoning_effort), @@ -211,13 +204,9 @@ impl CollaborationMode { .or_else(|| settings.developer_instructions.clone()), }; - match self { - CollaborationMode::Plan(_) => CollaborationMode::Plan(updated_settings), - CollaborationMode::PairProgramming(_) => { - CollaborationMode::PairProgramming(updated_settings) - } - CollaborationMode::Execute(_) => CollaborationMode::Execute(updated_settings), - CollaborationMode::Custom(_) => CollaborationMode::Custom(updated_settings), + CollaborationMode { + mode: self.mode, + settings: updated_settings, } } } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index f173d39645..57241bb8b6 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -263,13 +263,8 @@ impl DeveloperInstructions { /// Returns developer instructions from a collaboration mode if they exist and are non-empty. pub fn from_collaboration_mode(collaboration_mode: &CollaborationMode) -> Option { - let settings = match collaboration_mode { - CollaborationMode::Plan(settings) - | CollaborationMode::PairProgramming(settings) - | CollaborationMode::Execute(settings) - | CollaborationMode::Custom(settings) => settings, - }; - settings + collaboration_mode + .settings .developer_instructions .as_ref() .filter(|instructions| !instructions.is_empty()) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 137e0f3195..89f6a96c5c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -910,7 +910,7 @@ impl ChatWidget { if !self.queued_user_messages.is_empty() { return; } - if !matches!(self.stored_collaboration_mode, CollaborationMode::Plan(_)) { + if self.stored_collaboration_mode.mode != ModeKind::Plan { return; } let has_message = last_agent_message.is_some_and(|message| !message.trim().is_empty()); @@ -1943,7 +1943,10 @@ impl ChatWidget { config.experimental_mode, ) } else { - CollaborationMode::Custom(fallback_custom) + CollaborationMode { + mode: ModeKind::Custom, + settings: fallback_custom, + } }; let active_cell = Some(Self::placeholder_session_header_cell(&config)); @@ -2061,7 +2064,10 @@ impl ChatWidget { config.experimental_mode, ) } else { - CollaborationMode::Custom(fallback_custom) + CollaborationMode { + mode: ModeKind::Custom, + settings: fallback_custom, + } }; let active_cell = Some(Self::placeholder_session_header_cell(&config)); @@ -2182,7 +2188,10 @@ impl ChatWidget { config.experimental_mode, ) } else { - CollaborationMode::Custom(fallback_custom) + CollaborationMode { + mode: ModeKind::Custom, + settings: fallback_custom, + } }; let mut widget = Self { @@ -3552,11 +3561,11 @@ impl ChatWidget { let items: Vec = presets .into_iter() .map(|preset| { - let name = match preset { - CollaborationMode::Plan(_) => "Plan", - CollaborationMode::PairProgramming(_) => "Pair Programming", - CollaborationMode::Execute(_) => "Execute", - CollaborationMode::Custom(_) => "Custom", + let name = match preset.mode { + ModeKind::Plan => "Plan", + ModeKind::PairProgramming => "Pair Programming", + ModeKind::Execute => "Execute", + ModeKind::Custom => "Custom", }; let is_current = collaboration_modes::same_variant(&self.stored_collaboration_mode, &preset); @@ -4538,12 +4547,7 @@ impl ChatWidget { } if feature == Feature::CollaborationModes { self.bottom_pane.set_collaboration_modes_enabled(enabled); - let settings = match &self.stored_collaboration_mode { - CollaborationMode::Plan(settings) - | CollaborationMode::PairProgramming(settings) - | CollaborationMode::Execute(settings) - | CollaborationMode::Custom(settings) => settings.clone(), - }; + let settings = self.stored_collaboration_mode.settings.clone(); let fallback_custom = settings.clone(); self.stored_collaboration_mode = if enabled { initial_collaboration_mode( @@ -4552,7 +4556,10 @@ impl ChatWidget { self.config.experimental_mode, ) } else { - CollaborationMode::Custom(settings) + CollaborationMode { + mode: ModeKind::Custom, + settings, + } }; self.update_collaboration_mode_indicator(); } @@ -4632,11 +4639,11 @@ impl ChatWidget { if !self.collaboration_modes_enabled() { return None; } - match &self.stored_collaboration_mode { - CollaborationMode::Plan(_) => Some("Plan"), - CollaborationMode::PairProgramming(_) => Some("Pair Programming"), - CollaborationMode::Execute(_) => Some("Execute"), - CollaborationMode::Custom(_) => None, + match self.stored_collaboration_mode.mode { + ModeKind::Plan => Some("Plan"), + ModeKind::PairProgramming => Some("Pair Programming"), + ModeKind::Execute => Some("Execute"), + ModeKind::Custom => None, } } @@ -4644,13 +4651,11 @@ impl ChatWidget { if !self.collaboration_modes_enabled() { return None; } - match &self.stored_collaboration_mode { - CollaborationMode::Plan(_) => Some(CollaborationModeIndicator::Plan), - CollaborationMode::PairProgramming(_) => { - Some(CollaborationModeIndicator::PairProgramming) - } - CollaborationMode::Execute(_) => Some(CollaborationModeIndicator::Execute), - CollaborationMode::Custom(_) => None, + match self.stored_collaboration_mode.mode { + ModeKind::Plan => Some(CollaborationModeIndicator::Plan), + ModeKind::PairProgramming => Some(CollaborationModeIndicator::PairProgramming), + ModeKind::Execute => Some(CollaborationModeIndicator::Execute), + ModeKind::Custom => None, } } @@ -4681,7 +4686,8 @@ impl ChatWidget { if !self.collaboration_modes_enabled() { return; } - + let old_model = self.stored_collaboration_mode.model().to_string(); + let mode = mode.with_updates(Some(old_model), None, None); self.stored_collaboration_mode = mode; self.update_collaboration_mode_indicator(); self.request_redraw(); @@ -5304,15 +5310,20 @@ fn initial_collaboration_mode( ) -> CollaborationMode { if let Some(kind) = desired_mode { if kind == ModeKind::Custom { - return CollaborationMode::Custom(fallback_custom); + return CollaborationMode { + mode: ModeKind::Custom, + settings: fallback_custom, + }; } if let Some(mode) = collaboration_modes::mode_for_kind(models_manager, kind) { return mode; } } - collaboration_modes::default_mode(models_manager) - .unwrap_or(CollaborationMode::Custom(fallback_custom)) + collaboration_modes::default_mode(models_manager).unwrap_or(CollaborationMode { + mode: ModeKind::Custom, + settings: fallback_custom, + }) } async fn fetch_rate_limits(base_url: String, auth: CodexAuth) -> Option { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 1fb199b0d8..9fe7d3a1c9 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -64,6 +64,8 @@ use codex_otel::OtelManager; use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; +use codex_protocol::config_types::Settings; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffortPreset; use codex_protocol::parse_command::ParsedCommand; @@ -779,18 +781,24 @@ async fn make_chatwidget_manual( let reasoning_effort = None; let stored_collaboration_mode = if collaboration_modes_enabled { collaboration_modes::default_mode(models_manager.as_ref()).unwrap_or_else(|| { - CollaborationMode::Custom(Settings { + CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { + model: resolved_model.clone(), + reasoning_effort, + developer_instructions: None, + }, + } + }) + } else { + CollaborationMode { + mode: ModeKind::Custom, + settings: Settings { model: resolved_model.clone(), reasoning_effort, developer_instructions: None, - }) - }) - } else { - CollaborationMode::Custom(Settings { - model: resolved_model.clone(), - reasoning_effort, - developer_instructions: None, - }) + }, + } }; let widget = ChatWidget { app_event_tx, @@ -1206,7 +1214,7 @@ async fn plan_implementation_popup_yes_emits_submit_message_event() { panic!("expected SubmitUserMessageWithMode, got {event:?}"); }; assert_eq!(text, PLAN_IMPLEMENTATION_EXECUTE_MESSAGE); - assert!(matches!(collaboration_mode, CollaborationMode::Execute(_))); + assert_eq!(collaboration_mode.mode, ModeKind::Execute); } #[tokio::test] @@ -1221,7 +1229,11 @@ async fn submit_user_message_with_mode_sets_execute_collaboration_mode() { match next_submit_op(&mut op_rx) { Op::UserTurn { - collaboration_mode: Some(CollaborationMode::Execute(_)), + collaboration_mode: + Some(CollaborationMode { + mode: ModeKind::Execute, + .. + }), personality: None, .. } => {} @@ -1235,11 +1247,14 @@ async fn submit_user_message_with_mode_sets_execute_collaboration_mode() { async fn plan_implementation_popup_skips_replayed_turn_complete() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; chat.set_feature_enabled(Feature::CollaborationModes, true); - chat.stored_collaboration_mode = CollaborationMode::Plan(Settings { - model: chat.current_model().to_string(), - reasoning_effort: None, - developer_instructions: None, - }); + chat.stored_collaboration_mode = CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: chat.current_model().to_string(), + reasoning_effort: None, + developer_instructions: None, + }, + }; chat.replay_initial_messages(vec![EventMsg::TurnComplete(TurnCompleteEvent { last_agent_message: Some("Plan details".to_string()), @@ -1256,11 +1271,14 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() { async fn plan_implementation_popup_skips_when_messages_queued() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; chat.set_feature_enabled(Feature::CollaborationModes, true); - chat.stored_collaboration_mode = CollaborationMode::Plan(Settings { - model: chat.current_model().to_string(), - reasoning_effort: None, - developer_instructions: None, - }); + chat.stored_collaboration_mode = CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: chat.current_model().to_string(), + reasoning_effort: None, + developer_instructions: None, + }, + }; chat.bottom_pane.set_task_running(true); chat.queue_user_message("Queued message".into()); @@ -1277,11 +1295,14 @@ async fn plan_implementation_popup_skips_when_messages_queued() { async fn plan_implementation_popup_shows_on_plan_update_without_message() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; chat.set_feature_enabled(Feature::CollaborationModes, true); - chat.stored_collaboration_mode = CollaborationMode::Plan(Settings { - model: chat.current_model().to_string(), - reasoning_effort: None, - developer_instructions: None, - }); + chat.stored_collaboration_mode = CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: chat.current_model().to_string(), + reasoning_effort: None, + developer_instructions: None, + }, + }; chat.on_task_started(); chat.on_plan_update(UpdatePlanArgs { @@ -1306,11 +1327,14 @@ async fn plan_implementation_popup_skips_when_rate_limit_prompt_pending() { chat.auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); chat.set_feature_enabled(Feature::CollaborationModes, true); - chat.stored_collaboration_mode = CollaborationMode::Plan(Settings { - model: chat.current_model().to_string(), - reasoning_effort: None, - developer_instructions: None, - }); + chat.stored_collaboration_mode = CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: chat.current_model().to_string(), + reasoning_effort: None, + developer_instructions: None, + }, + }; chat.on_task_started(); chat.on_plan_update(UpdatePlanArgs { @@ -2205,16 +2229,10 @@ async fn collab_mode_shift_tab_cycles_only_when_enabled_and_idle() { chat.set_feature_enabled(Feature::CollaborationModes, true); chat.handle_key_event(KeyEvent::from(KeyCode::BackTab)); - assert!(matches!( - chat.stored_collaboration_mode, - CollaborationMode::Execute(_) - )); + assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Execute); chat.handle_key_event(KeyEvent::from(KeyCode::BackTab)); - assert!(matches!( - chat.stored_collaboration_mode, - CollaborationMode::Plan(_) - )); + assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Plan); chat.on_task_started(); let before = chat.stored_collaboration_mode.clone(); @@ -2247,7 +2265,11 @@ async fn collab_slash_command_opens_picker_and_updates_mode() { chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); match next_submit_op(&mut op_rx) { Op::UserTurn { - collaboration_mode: Some(CollaborationMode::PairProgramming(_)), + collaboration_mode: + Some(CollaborationMode { + mode: ModeKind::PairProgramming, + .. + }), personality: None, .. } => {} @@ -2261,7 +2283,11 @@ async fn collab_slash_command_opens_picker_and_updates_mode() { chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); match next_submit_op(&mut op_rx) { Op::UserTurn { - collaboration_mode: Some(CollaborationMode::PairProgramming(_)), + collaboration_mode: + Some(CollaborationMode { + mode: ModeKind::PairProgramming, + .. + }), personality: None, .. } => {} @@ -2282,7 +2308,11 @@ async fn collab_mode_defaults_to_pair_programming_when_enabled() { chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); match next_submit_op(&mut op_rx) { Op::UserTurn { - collaboration_mode: Some(CollaborationMode::PairProgramming(_)), + collaboration_mode: + Some(CollaborationMode { + mode: ModeKind::PairProgramming, + .. + }), personality: None, .. } => {} @@ -2296,10 +2326,10 @@ async fn collab_mode_defaults_to_pair_programming_when_enabled() { async fn collab_mode_enabling_sets_pair_programming_default() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; chat.set_feature_enabled(Feature::CollaborationModes, true); - assert!(matches!( - chat.stored_collaboration_mode, - CollaborationMode::PairProgramming(_) - )); + assert_eq!( + chat.stored_collaboration_mode.mode, + ModeKind::PairProgramming + ); } #[tokio::test] diff --git a/codex-rs/tui/src/collaboration_modes.rs b/codex-rs/tui/src/collaboration_modes.rs index 7059165552..df034053d4 100644 --- a/codex-rs/tui/src/collaboration_modes.rs +++ b/codex-rs/tui/src/collaboration_modes.rs @@ -3,19 +3,14 @@ use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; fn mode_kind(mode: &CollaborationMode) -> ModeKind { - match mode { - CollaborationMode::Plan(_) => ModeKind::Plan, - CollaborationMode::PairProgramming(_) => ModeKind::PairProgramming, - CollaborationMode::Execute(_) => ModeKind::Execute, - CollaborationMode::Custom(_) => ModeKind::Custom, - } + mode.mode } pub(crate) fn default_mode(models_manager: &ModelsManager) -> Option { let presets = models_manager.list_collaboration_modes(); presets .iter() - .find(|preset| matches!(preset, CollaborationMode::PairProgramming(_))) + .find(|preset| preset.mode == ModeKind::PairProgramming) .cloned() .or_else(|| presets.into_iter().next()) }