change collaboration mode to struct (#9793)

Shouldn't cause behavioral change
This commit is contained in:
Ahmed Ibrahim
2026-01-23 17:00:23 -08:00
committed by GitHub
parent 1167465bf6
commit 69cfc73dc6
16 changed files with 291 additions and 210 deletions
@@ -17,6 +17,7 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId; use codex_app_server_protocol::RequestId;
use codex_core::models_manager::test_builtin_collaboration_mode_presets; use codex_core::models_manager::test_builtin_collaboration_mode_presets;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use tempfile::TempDir; use tempfile::TempDir;
use tokio::time::timeout; use tokio::time::timeout;
@@ -57,7 +58,7 @@ fn plan_preset() -> CollaborationMode {
let presets = test_builtin_collaboration_mode_presets(); let presets = test_builtin_collaboration_mode_presets();
presets presets
.into_iter() .into_iter()
.find(|p| matches!(p, CollaborationMode::Plan(_))) .find(|p| p.mode == ModeKind::Plan)
.unwrap() .unwrap()
} }
@@ -69,7 +70,7 @@ fn pair_programming_preset() -> CollaborationMode {
let presets = test_builtin_collaboration_mode_presets(); let presets = test_builtin_collaboration_mode_presets();
presets presets
.into_iter() .into_iter()
.find(|p| matches!(p, CollaborationMode::PairProgramming(_))) .find(|p| p.mode == ModeKind::PairProgramming)
.unwrap() .unwrap()
} }
@@ -81,6 +82,6 @@ fn execute_preset() -> CollaborationMode {
let presets = test_builtin_collaboration_mode_presets(); let presets = test_builtin_collaboration_mode_presets();
presets presets
.into_iter() .into_iter()
.find(|p| matches!(p, CollaborationMode::Execute(_))) .find(|p| p.mode == ModeKind::Execute)
.unwrap() .unwrap()
} }
@@ -13,6 +13,7 @@ use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::UserInput as V2UserInput; use codex_app_server_protocol::UserInput as V2UserInput;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings; use codex_protocol::config_types::Settings;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
use tokio::time::timeout; use tokio::time::timeout;
@@ -54,11 +55,14 @@ async fn request_user_input_round_trip() -> Result<()> {
}], }],
model: Some("mock-model".to_string()), model: Some("mock-model".to_string()),
effort: Some(ReasoningEffort::Medium), effort: Some(ReasoningEffort::Medium),
collaboration_mode: Some(CollaborationMode::Plan(Settings { collaboration_mode: Some(CollaborationMode {
model: "mock-model".to_string(), mode: ModeKind::Plan,
reasoning_effort: Some(ReasoningEffort::Medium), settings: Settings {
developer_instructions: None, model: "mock-model".to_string(),
})), reasoning_effort: Some(ReasoningEffort::Medium),
developer_instructions: None,
},
}),
..Default::default() ..Default::default()
}) })
.await?; .await?;
@@ -38,6 +38,7 @@ use codex_core::features::FEATURES;
use codex_core::features::Feature; use codex_core::features::Feature;
use codex_core::protocol_config_types::ReasoningSummary; use codex_core::protocol_config_types::ReasoningSummary;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality; use codex_protocol::config_types::Personality;
use codex_protocol::config_types::Settings; use codex_protocol::config_types::Settings;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
@@ -363,11 +364,14 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> {
.await??; .await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?; let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let collaboration_mode = CollaborationMode::Custom(Settings { let collaboration_mode = CollaborationMode {
model: "mock-model-collab".to_string(), mode: ModeKind::Custom,
reasoning_effort: Some(ReasoningEffort::High), settings: Settings {
developer_instructions: None, model: "mock-model-collab".to_string(),
}); reasoning_effort: Some(ReasoningEffort::High),
developer_instructions: None,
},
};
let turn_req = mcp let turn_req = mcp
.send_turn_start_request(TurnStartParams { .send_turn_start_request(TurnStartParams {
+50 -30
View File
@@ -35,6 +35,7 @@ use async_channel::Receiver;
use async_channel::Sender; use async_channel::Sender;
use codex_protocol::ThreadId; use codex_protocol::ThreadId;
use codex_protocol::approvals::ExecPolicyAmendment; use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings; use codex_protocol::config_types::Settings;
use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchMode;
use codex_protocol::items::TurnItem; 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 // TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode
// to avoid extracting these fields separately and constructing CollaborationMode here. // to avoid extracting these fields separately and constructing CollaborationMode here.
let collaboration_mode = CollaborationMode::Custom(Settings { let collaboration_mode = CollaborationMode {
model: model.clone(), mode: ModeKind::Custom,
reasoning_effort: config.model_reasoning_effort, settings: Settings {
developer_instructions: None, model: model.clone(),
}); reasoning_effort: config.model_reasoning_effort,
developer_instructions: None,
},
};
let session_configuration = SessionConfiguration { let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(), provider: config.model_provider.clone(),
collaboration_mode, collaboration_mode,
@@ -2215,6 +2219,7 @@ mod handlers {
use crate::context_manager::is_user_turn_boundary; use crate::context_manager::is_user_turn_boundary;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings; use codex_protocol::config_types::Settings;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_rmcp_client::ElicitationAction; use codex_rmcp_client::ElicitationAction;
@@ -2291,11 +2296,14 @@ mod handlers {
personality, personality,
} => { } => {
let collaboration_mode = collaboration_mode.or_else(|| { let collaboration_mode = collaboration_mode.or_else(|| {
Some(CollaborationMode::Custom(Settings { Some(CollaborationMode {
model: model.clone(), mode: ModeKind::Custom,
reasoning_effort: effort, settings: Settings {
developer_instructions: None, model: model.clone(),
})) reasoning_effort: effort,
developer_instructions: None,
},
})
}); });
( (
items, items,
@@ -3878,11 +3886,14 @@ mod tests {
let model = ModelsManager::get_model_offline(config.model.as_deref()); let model = ModelsManager::get_model_offline(config.model.as_deref());
let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config);
let reasoning_effort = config.model_reasoning_effort; let reasoning_effort = config.model_reasoning_effort;
let collaboration_mode = CollaborationMode::Custom(Settings { let collaboration_mode = CollaborationMode {
model, mode: ModeKind::Custom,
reasoning_effort, settings: Settings {
developer_instructions: None, model,
}); reasoning_effort,
developer_instructions: None,
},
};
let session_configuration = SessionConfiguration { let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(), provider: config.model_provider.clone(),
collaboration_mode, collaboration_mode,
@@ -3954,11 +3965,14 @@ mod tests {
let model = ModelsManager::get_model_offline(config.model.as_deref()); let model = ModelsManager::get_model_offline(config.model.as_deref());
let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config);
let reasoning_effort = config.model_reasoning_effort; let reasoning_effort = config.model_reasoning_effort;
let collaboration_mode = CollaborationMode::Custom(Settings { let collaboration_mode = CollaborationMode {
model, mode: ModeKind::Custom,
reasoning_effort, settings: Settings {
developer_instructions: None, model,
}); reasoning_effort,
developer_instructions: None,
},
};
let session_configuration = SessionConfiguration { let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(), provider: config.model_provider.clone(),
collaboration_mode, collaboration_mode,
@@ -4214,11 +4228,14 @@ mod tests {
let model = ModelsManager::get_model_offline(config.model.as_deref()); let model = ModelsManager::get_model_offline(config.model.as_deref());
let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config);
let reasoning_effort = config.model_reasoning_effort; let reasoning_effort = config.model_reasoning_effort;
let collaboration_mode = CollaborationMode::Custom(Settings { let collaboration_mode = CollaborationMode {
model, mode: ModeKind::Custom,
reasoning_effort, settings: Settings {
developer_instructions: None, model,
}); reasoning_effort,
developer_instructions: None,
},
};
let session_configuration = SessionConfiguration { let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(), provider: config.model_provider.clone(),
collaboration_mode, collaboration_mode,
@@ -4319,11 +4336,14 @@ mod tests {
let model = ModelsManager::get_model_offline(config.model.as_deref()); let model = ModelsManager::get_model_offline(config.model.as_deref());
let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config);
let reasoning_effort = config.model_reasoning_effort; let reasoning_effort = config.model_reasoning_effort;
let collaboration_mode = CollaborationMode::Custom(Settings { let collaboration_mode = CollaborationMode {
model, mode: ModeKind::Custom,
reasoning_effort, settings: Settings {
developer_instructions: None, model,
}); reasoning_effort,
developer_instructions: None,
},
};
let session_configuration = SessionConfiguration { let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(), provider: config.model_provider.clone(),
collaboration_mode, collaboration_mode,
@@ -1,4 +1,5 @@
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings; use codex_protocol::config_types::Settings;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
@@ -18,25 +19,34 @@ pub fn test_builtin_collaboration_mode_presets() -> Vec<CollaborationMode> {
} }
fn plan_preset() -> CollaborationMode { fn plan_preset() -> CollaborationMode {
CollaborationMode::Plan(Settings { CollaborationMode {
model: "gpt-5.2-codex".to_string(), mode: ModeKind::Plan,
reasoning_effort: Some(ReasoningEffort::High), settings: Settings {
developer_instructions: Some(COLLABORATION_MODE_PLAN.to_string()), 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 { fn pair_programming_preset() -> CollaborationMode {
CollaborationMode::PairProgramming(Settings { CollaborationMode {
model: "gpt-5.2-codex".to_string(), mode: ModeKind::PairProgramming,
reasoning_effort: Some(ReasoningEffort::Medium), settings: Settings {
developer_instructions: Some(COLLABORATION_MODE_PAIR_PROGRAMMING.to_string()), 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 { fn execute_preset() -> CollaborationMode {
CollaborationMode::Execute(Settings { CollaborationMode {
model: "gpt-5.2-codex".to_string(), mode: ModeKind::Execute,
reasoning_effort: Some(ReasoningEffort::High), settings: Settings {
developer_instructions: Some(COLLABORATION_MODE_EXECUTE.to_string()), model: "gpt-5.2-codex".to_string(),
}) reasoning_effort: Some(ReasoningEffort::High),
developer_instructions: Some(COLLABORATION_MODE_EXECUTE.to_string()),
},
}
} }
@@ -7,7 +7,7 @@ use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments; use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind; use crate::tools::registry::ToolKind;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind;
use codex_protocol::request_user_input::RequestUserInputArgs; use codex_protocol::request_user_input::RequestUserInputArgs;
pub struct RequestUserInputHandler; pub struct RequestUserInputHandler;
@@ -36,9 +36,9 @@ impl ToolHandler for RequestUserInputHandler {
} }
}; };
let disallowed_mode = match session.collaboration_mode().await { let disallowed_mode = match session.collaboration_mode().await.mode {
CollaborationMode::Execute(_) => Some("Execute"), ModeKind::Execute => Some("Execute"),
CollaborationMode::Custom(_) => Some("Custom"), ModeKind::Custom => Some("Custom"),
_ => None, _ => None,
}; };
if let Some(mode_name) = disallowed_mode { if let Some(mode_name) = disallowed_mode {
+9 -5
View File
@@ -23,6 +23,7 @@ use codex_core::protocol::SessionSource;
use codex_otel::OtelManager; use codex_otel::OtelManager;
use codex_protocol::ThreadId; use codex_protocol::ThreadId;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings; use codex_protocol::config_types::Settings;
use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::Verbosity;
@@ -887,11 +888,14 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
.build(&server) .build(&server)
.await?; .await?;
let collaboration_mode = CollaborationMode::Custom(Settings { let collaboration_mode = CollaborationMode {
model: "gpt-5.1".to_string(), mode: ModeKind::Custom,
reasoning_effort: Some(ReasoningEffort::High), settings: Settings {
developer_instructions: None, model: "gpt-5.1".to_string(),
}); reasoning_effort: Some(ReasoningEffort::High),
developer_instructions: None,
},
};
codex codex
.submit(Op::UserTurn { .submit(Op::UserTurn {
@@ -4,6 +4,7 @@ use codex_core::protocol::COLLABORATION_MODE_OPEN_TAG;
use codex_core::protocol::EventMsg; use codex_core::protocol::EventMsg;
use codex_core::protocol::Op; use codex_core::protocol::Op;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings; use codex_protocol::config_types::Settings;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_completed; 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 { fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMode {
CollaborationMode::Custom(Settings { CollaborationMode {
model: "gpt-5.1".to_string(), mode: ModeKind::Custom,
reasoning_effort: None, settings: Settings {
developer_instructions: instructions.map(str::to_string), model: "gpt-5.1".to_string(),
}) reasoning_effort: None,
developer_instructions: instructions.map(str::to_string),
},
}
} }
fn developer_texts(input: &[Value]) -> Vec<String> { fn developer_texts(input: &[Value]) -> Vec<String> {
@@ -9,6 +9,7 @@ use codex_core::protocol::RolloutItem;
use codex_core::protocol::RolloutLine; use codex_core::protocol::RolloutLine;
use codex_core::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; use codex_core::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings; use codex_protocol::config_types::Settings;
use codex_protocol::models::ContentItem; use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
@@ -23,11 +24,14 @@ use std::time::Duration;
use tempfile::TempDir; use tempfile::TempDir;
fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMode { fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMode {
CollaborationMode::Custom(Settings { CollaborationMode {
model: "gpt-5.1".to_string(), mode: ModeKind::Custom,
reasoning_effort: None, settings: Settings {
developer_instructions: instructions.map(str::to_string), model: "gpt-5.1".to_string(),
}) reasoning_effort: None,
developer_instructions: instructions.map(str::to_string),
},
}
} }
fn collab_xml(text: &str) -> String { fn collab_xml(text: &str) -> String {
+9 -5
View File
@@ -12,6 +12,7 @@ use codex_core::protocol_config_types::ReasoningSummary;
use codex_core::shell::Shell; use codex_core::shell::Shell;
use codex_core::shell::default_user_shell; use codex_core::shell::default_user_shell;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings; use codex_protocol::config_types::Settings;
use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchMode;
use codex_protocol::openai_models::ReasoningEffort; 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 TestCodex { codex, .. } = test_codex().build(&server).await?;
let collaboration_mode = CollaborationMode::Custom(Settings { let collaboration_mode = CollaborationMode {
model: "gpt-5.1".to_string(), mode: ModeKind::Custom,
reasoning_effort: Some(ReasoningEffort::High), settings: Settings {
developer_instructions: None, model: "gpt-5.1".to_string(),
}); reasoning_effort: Some(ReasoningEffort::High),
developer_instructions: None,
},
};
codex codex
.submit(Op::OverrideTurnContext { .submit(Op::OverrideTurnContext {
+17 -11
View File
@@ -8,6 +8,7 @@ use codex_core::protocol::EventMsg;
use codex_core::protocol::Op; use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SandboxPolicy;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings; use codex_protocol::config_types::Settings;
use codex_protocol::request_user_input::RequestUserInputAnswer; 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, model: session_model,
effort: None, effort: None,
summary: ReasoningSummary::Auto, summary: ReasoningSummary::Auto,
collaboration_mode: Some(CollaborationMode::Plan(Settings { collaboration_mode: Some(CollaborationMode {
model: session_configured.model.clone(), mode: ModeKind::Plan,
reasoning_effort: None, settings: Settings {
developer_instructions: None, model: session_configured.model.clone(),
})), reasoning_effort: None,
developer_instructions: None,
},
}),
personality: None, personality: None,
}) })
.await?; .await?;
@@ -269,24 +273,26 @@ where
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_rejected_in_execute_mode() -> anyhow::Result<()> { async fn request_user_input_rejected_in_execute_mode() -> anyhow::Result<()> {
assert_request_user_input_rejected("Execute", |model| { assert_request_user_input_rejected("Execute", |model| CollaborationMode {
CollaborationMode::Execute(Settings { mode: ModeKind::Execute,
settings: Settings {
model, model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}) },
}) })
.await .await
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_rejected_in_custom_mode() -> anyhow::Result<()> { async fn request_user_input_rejected_in_custom_mode() -> anyhow::Result<()> {
assert_request_user_input_rejected("Custom", |model| { assert_request_user_input_rejected("Custom", |model| CollaborationMode {
CollaborationMode::Custom(Settings { mode: ModeKind::Custom,
settings: Settings {
model, model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}) },
}) })
.await .await
} }
+14 -25
View File
@@ -163,31 +163,24 @@ pub enum ModeKind {
/// Collaboration mode for a Codex session. /// Collaboration mode for a Codex session.
#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)] #[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)]
#[serde(tag = "mode", rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum CollaborationMode { pub struct CollaborationMode {
Plan(Settings), pub mode: ModeKind,
PairProgramming(Settings), pub settings: Settings,
Execute(Settings),
Custom(Settings),
} }
impl CollaborationMode { impl CollaborationMode {
/// Returns a reference to the settings, regardless of variant. /// Returns a reference to the settings.
fn settings(&self) -> &Settings { fn settings_ref(&self) -> &Settings {
match self { &self.settings
CollaborationMode::Plan(settings)
| CollaborationMode::PairProgramming(settings)
| CollaborationMode::Execute(settings)
| CollaborationMode::Custom(settings) => settings,
}
} }
pub fn model(&self) -> &str { pub fn model(&self) -> &str {
self.settings().model.as_str() self.settings_ref().model.as_str()
} }
pub fn reasoning_effort(&self) -> Option<ReasoningEffort> { pub fn reasoning_effort(&self) -> Option<ReasoningEffort> {
self.settings().reasoning_effort self.settings_ref().reasoning_effort
} }
/// Updates the collaboration mode with new model and/or effort values. /// 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 /// - `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 /// - `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( pub fn with_updates(
&self, &self,
model: Option<String>, model: Option<String>,
effort: Option<Option<ReasoningEffort>>, effort: Option<Option<ReasoningEffort>>,
developer_instructions: Option<String>, developer_instructions: Option<String>,
) -> Self { ) -> Self {
let settings = self.settings(); let settings = self.settings_ref();
let updated_settings = Settings { let updated_settings = Settings {
model: model.unwrap_or_else(|| settings.model.clone()), model: model.unwrap_or_else(|| settings.model.clone()),
reasoning_effort: effort.unwrap_or(settings.reasoning_effort), reasoning_effort: effort.unwrap_or(settings.reasoning_effort),
@@ -211,13 +204,9 @@ impl CollaborationMode {
.or_else(|| settings.developer_instructions.clone()), .or_else(|| settings.developer_instructions.clone()),
}; };
match self { CollaborationMode {
CollaborationMode::Plan(_) => CollaborationMode::Plan(updated_settings), mode: self.mode,
CollaborationMode::PairProgramming(_) => { settings: updated_settings,
CollaborationMode::PairProgramming(updated_settings)
}
CollaborationMode::Execute(_) => CollaborationMode::Execute(updated_settings),
CollaborationMode::Custom(_) => CollaborationMode::Custom(updated_settings),
} }
} }
} }
+2 -7
View File
@@ -263,13 +263,8 @@ impl DeveloperInstructions {
/// Returns developer instructions from a collaboration mode if they exist and are non-empty. /// Returns developer instructions from a collaboration mode if they exist and are non-empty.
pub fn from_collaboration_mode(collaboration_mode: &CollaborationMode) -> Option<Self> { pub fn from_collaboration_mode(collaboration_mode: &CollaborationMode) -> Option<Self> {
let settings = match collaboration_mode { collaboration_mode
CollaborationMode::Plan(settings) .settings
| CollaborationMode::PairProgramming(settings)
| CollaborationMode::Execute(settings)
| CollaborationMode::Custom(settings) => settings,
};
settings
.developer_instructions .developer_instructions
.as_ref() .as_ref()
.filter(|instructions| !instructions.is_empty()) .filter(|instructions| !instructions.is_empty())
+43 -32
View File
@@ -910,7 +910,7 @@ impl ChatWidget {
if !self.queued_user_messages.is_empty() { if !self.queued_user_messages.is_empty() {
return; return;
} }
if !matches!(self.stored_collaboration_mode, CollaborationMode::Plan(_)) { if self.stored_collaboration_mode.mode != ModeKind::Plan {
return; return;
} }
let has_message = last_agent_message.is_some_and(|message| !message.trim().is_empty()); let has_message = last_agent_message.is_some_and(|message| !message.trim().is_empty());
@@ -1943,7 +1943,10 @@ impl ChatWidget {
config.experimental_mode, config.experimental_mode,
) )
} else { } else {
CollaborationMode::Custom(fallback_custom) CollaborationMode {
mode: ModeKind::Custom,
settings: fallback_custom,
}
}; };
let active_cell = Some(Self::placeholder_session_header_cell(&config)); let active_cell = Some(Self::placeholder_session_header_cell(&config));
@@ -2061,7 +2064,10 @@ impl ChatWidget {
config.experimental_mode, config.experimental_mode,
) )
} else { } else {
CollaborationMode::Custom(fallback_custom) CollaborationMode {
mode: ModeKind::Custom,
settings: fallback_custom,
}
}; };
let active_cell = Some(Self::placeholder_session_header_cell(&config)); let active_cell = Some(Self::placeholder_session_header_cell(&config));
@@ -2182,7 +2188,10 @@ impl ChatWidget {
config.experimental_mode, config.experimental_mode,
) )
} else { } else {
CollaborationMode::Custom(fallback_custom) CollaborationMode {
mode: ModeKind::Custom,
settings: fallback_custom,
}
}; };
let mut widget = Self { let mut widget = Self {
@@ -3552,11 +3561,11 @@ impl ChatWidget {
let items: Vec<SelectionItem> = presets let items: Vec<SelectionItem> = presets
.into_iter() .into_iter()
.map(|preset| { .map(|preset| {
let name = match preset { let name = match preset.mode {
CollaborationMode::Plan(_) => "Plan", ModeKind::Plan => "Plan",
CollaborationMode::PairProgramming(_) => "Pair Programming", ModeKind::PairProgramming => "Pair Programming",
CollaborationMode::Execute(_) => "Execute", ModeKind::Execute => "Execute",
CollaborationMode::Custom(_) => "Custom", ModeKind::Custom => "Custom",
}; };
let is_current = let is_current =
collaboration_modes::same_variant(&self.stored_collaboration_mode, &preset); collaboration_modes::same_variant(&self.stored_collaboration_mode, &preset);
@@ -4538,12 +4547,7 @@ impl ChatWidget {
} }
if feature == Feature::CollaborationModes { if feature == Feature::CollaborationModes {
self.bottom_pane.set_collaboration_modes_enabled(enabled); self.bottom_pane.set_collaboration_modes_enabled(enabled);
let settings = match &self.stored_collaboration_mode { let settings = self.stored_collaboration_mode.settings.clone();
CollaborationMode::Plan(settings)
| CollaborationMode::PairProgramming(settings)
| CollaborationMode::Execute(settings)
| CollaborationMode::Custom(settings) => settings.clone(),
};
let fallback_custom = settings.clone(); let fallback_custom = settings.clone();
self.stored_collaboration_mode = if enabled { self.stored_collaboration_mode = if enabled {
initial_collaboration_mode( initial_collaboration_mode(
@@ -4552,7 +4556,10 @@ impl ChatWidget {
self.config.experimental_mode, self.config.experimental_mode,
) )
} else { } else {
CollaborationMode::Custom(settings) CollaborationMode {
mode: ModeKind::Custom,
settings,
}
}; };
self.update_collaboration_mode_indicator(); self.update_collaboration_mode_indicator();
} }
@@ -4632,11 +4639,11 @@ impl ChatWidget {
if !self.collaboration_modes_enabled() { if !self.collaboration_modes_enabled() {
return None; return None;
} }
match &self.stored_collaboration_mode { match self.stored_collaboration_mode.mode {
CollaborationMode::Plan(_) => Some("Plan"), ModeKind::Plan => Some("Plan"),
CollaborationMode::PairProgramming(_) => Some("Pair Programming"), ModeKind::PairProgramming => Some("Pair Programming"),
CollaborationMode::Execute(_) => Some("Execute"), ModeKind::Execute => Some("Execute"),
CollaborationMode::Custom(_) => None, ModeKind::Custom => None,
} }
} }
@@ -4644,13 +4651,11 @@ impl ChatWidget {
if !self.collaboration_modes_enabled() { if !self.collaboration_modes_enabled() {
return None; return None;
} }
match &self.stored_collaboration_mode { match self.stored_collaboration_mode.mode {
CollaborationMode::Plan(_) => Some(CollaborationModeIndicator::Plan), ModeKind::Plan => Some(CollaborationModeIndicator::Plan),
CollaborationMode::PairProgramming(_) => { ModeKind::PairProgramming => Some(CollaborationModeIndicator::PairProgramming),
Some(CollaborationModeIndicator::PairProgramming) ModeKind::Execute => Some(CollaborationModeIndicator::Execute),
} ModeKind::Custom => None,
CollaborationMode::Execute(_) => Some(CollaborationModeIndicator::Execute),
CollaborationMode::Custom(_) => None,
} }
} }
@@ -4681,7 +4686,8 @@ impl ChatWidget {
if !self.collaboration_modes_enabled() { if !self.collaboration_modes_enabled() {
return; 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.stored_collaboration_mode = mode;
self.update_collaboration_mode_indicator(); self.update_collaboration_mode_indicator();
self.request_redraw(); self.request_redraw();
@@ -5304,15 +5310,20 @@ fn initial_collaboration_mode(
) -> CollaborationMode { ) -> CollaborationMode {
if let Some(kind) = desired_mode { if let Some(kind) = desired_mode {
if kind == ModeKind::Custom { 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) { if let Some(mode) = collaboration_modes::mode_for_kind(models_manager, kind) {
return mode; return mode;
} }
} }
collaboration_modes::default_mode(models_manager) collaboration_modes::default_mode(models_manager).unwrap_or(CollaborationMode {
.unwrap_or(CollaborationMode::Custom(fallback_custom)) mode: ModeKind::Custom,
settings: fallback_custom,
})
} }
async fn fetch_rate_limits(base_url: String, auth: CodexAuth) -> Option<RateLimitSnapshot> { async fn fetch_rate_limits(base_url: String, auth: CodexAuth) -> Option<RateLimitSnapshot> {
+76 -46
View File
@@ -64,6 +64,8 @@ use codex_otel::OtelManager;
use codex_protocol::ThreadId; use codex_protocol::ThreadId;
use codex_protocol::account::PlanType; use codex_protocol::account::PlanType;
use codex_protocol::config_types::CollaborationMode; 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::ModelPreset;
use codex_protocol::openai_models::ReasoningEffortPreset; use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_protocol::parse_command::ParsedCommand; use codex_protocol::parse_command::ParsedCommand;
@@ -779,18 +781,24 @@ async fn make_chatwidget_manual(
let reasoning_effort = None; let reasoning_effort = None;
let stored_collaboration_mode = if collaboration_modes_enabled { let stored_collaboration_mode = if collaboration_modes_enabled {
collaboration_modes::default_mode(models_manager.as_ref()).unwrap_or_else(|| { 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(), model: resolved_model.clone(),
reasoning_effort, reasoning_effort,
developer_instructions: None, developer_instructions: None,
}) },
}) }
} else {
CollaborationMode::Custom(Settings {
model: resolved_model.clone(),
reasoning_effort,
developer_instructions: None,
})
}; };
let widget = ChatWidget { let widget = ChatWidget {
app_event_tx, app_event_tx,
@@ -1206,7 +1214,7 @@ async fn plan_implementation_popup_yes_emits_submit_message_event() {
panic!("expected SubmitUserMessageWithMode, got {event:?}"); panic!("expected SubmitUserMessageWithMode, got {event:?}");
}; };
assert_eq!(text, PLAN_IMPLEMENTATION_EXECUTE_MESSAGE); assert_eq!(text, PLAN_IMPLEMENTATION_EXECUTE_MESSAGE);
assert!(matches!(collaboration_mode, CollaborationMode::Execute(_))); assert_eq!(collaboration_mode.mode, ModeKind::Execute);
} }
#[tokio::test] #[tokio::test]
@@ -1221,7 +1229,11 @@ async fn submit_user_message_with_mode_sets_execute_collaboration_mode() {
match next_submit_op(&mut op_rx) { match next_submit_op(&mut op_rx) {
Op::UserTurn { Op::UserTurn {
collaboration_mode: Some(CollaborationMode::Execute(_)), collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::Execute,
..
}),
personality: None, 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() { async fn plan_implementation_popup_skips_replayed_turn_complete() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
chat.set_feature_enabled(Feature::CollaborationModes, true); chat.set_feature_enabled(Feature::CollaborationModes, true);
chat.stored_collaboration_mode = CollaborationMode::Plan(Settings { chat.stored_collaboration_mode = CollaborationMode {
model: chat.current_model().to_string(), mode: ModeKind::Plan,
reasoning_effort: None, settings: Settings {
developer_instructions: None, model: chat.current_model().to_string(),
}); reasoning_effort: None,
developer_instructions: None,
},
};
chat.replay_initial_messages(vec![EventMsg::TurnComplete(TurnCompleteEvent { chat.replay_initial_messages(vec![EventMsg::TurnComplete(TurnCompleteEvent {
last_agent_message: Some("Plan details".to_string()), 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() { async fn plan_implementation_popup_skips_when_messages_queued() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
chat.set_feature_enabled(Feature::CollaborationModes, true); chat.set_feature_enabled(Feature::CollaborationModes, true);
chat.stored_collaboration_mode = CollaborationMode::Plan(Settings { chat.stored_collaboration_mode = CollaborationMode {
model: chat.current_model().to_string(), mode: ModeKind::Plan,
reasoning_effort: None, settings: Settings {
developer_instructions: None, model: chat.current_model().to_string(),
}); reasoning_effort: None,
developer_instructions: None,
},
};
chat.bottom_pane.set_task_running(true); chat.bottom_pane.set_task_running(true);
chat.queue_user_message("Queued message".into()); 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() { async fn plan_implementation_popup_shows_on_plan_update_without_message() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
chat.set_feature_enabled(Feature::CollaborationModes, true); chat.set_feature_enabled(Feature::CollaborationModes, true);
chat.stored_collaboration_mode = CollaborationMode::Plan(Settings { chat.stored_collaboration_mode = CollaborationMode {
model: chat.current_model().to_string(), mode: ModeKind::Plan,
reasoning_effort: None, settings: Settings {
developer_instructions: None, model: chat.current_model().to_string(),
}); reasoning_effort: None,
developer_instructions: None,
},
};
chat.on_task_started(); chat.on_task_started();
chat.on_plan_update(UpdatePlanArgs { chat.on_plan_update(UpdatePlanArgs {
@@ -1306,11 +1327,14 @@ async fn plan_implementation_popup_skips_when_rate_limit_prompt_pending() {
chat.auth_manager = chat.auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
chat.set_feature_enabled(Feature::CollaborationModes, true); chat.set_feature_enabled(Feature::CollaborationModes, true);
chat.stored_collaboration_mode = CollaborationMode::Plan(Settings { chat.stored_collaboration_mode = CollaborationMode {
model: chat.current_model().to_string(), mode: ModeKind::Plan,
reasoning_effort: None, settings: Settings {
developer_instructions: None, model: chat.current_model().to_string(),
}); reasoning_effort: None,
developer_instructions: None,
},
};
chat.on_task_started(); chat.on_task_started();
chat.on_plan_update(UpdatePlanArgs { 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.set_feature_enabled(Feature::CollaborationModes, true);
chat.handle_key_event(KeyEvent::from(KeyCode::BackTab)); chat.handle_key_event(KeyEvent::from(KeyCode::BackTab));
assert!(matches!( assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Execute);
chat.stored_collaboration_mode,
CollaborationMode::Execute(_)
));
chat.handle_key_event(KeyEvent::from(KeyCode::BackTab)); chat.handle_key_event(KeyEvent::from(KeyCode::BackTab));
assert!(matches!( assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Plan);
chat.stored_collaboration_mode,
CollaborationMode::Plan(_)
));
chat.on_task_started(); chat.on_task_started();
let before = chat.stored_collaboration_mode.clone(); 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)); chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
match next_submit_op(&mut op_rx) { match next_submit_op(&mut op_rx) {
Op::UserTurn { Op::UserTurn {
collaboration_mode: Some(CollaborationMode::PairProgramming(_)), collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::PairProgramming,
..
}),
personality: None, personality: None,
.. ..
} => {} } => {}
@@ -2261,7 +2283,11 @@ async fn collab_slash_command_opens_picker_and_updates_mode() {
chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
match next_submit_op(&mut op_rx) { match next_submit_op(&mut op_rx) {
Op::UserTurn { Op::UserTurn {
collaboration_mode: Some(CollaborationMode::PairProgramming(_)), collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::PairProgramming,
..
}),
personality: None, personality: None,
.. ..
} => {} } => {}
@@ -2282,7 +2308,11 @@ async fn collab_mode_defaults_to_pair_programming_when_enabled() {
chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
match next_submit_op(&mut op_rx) { match next_submit_op(&mut op_rx) {
Op::UserTurn { Op::UserTurn {
collaboration_mode: Some(CollaborationMode::PairProgramming(_)), collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::PairProgramming,
..
}),
personality: None, 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() { async fn collab_mode_enabling_sets_pair_programming_default() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.set_feature_enabled(Feature::CollaborationModes, true); chat.set_feature_enabled(Feature::CollaborationModes, true);
assert!(matches!( assert_eq!(
chat.stored_collaboration_mode, chat.stored_collaboration_mode.mode,
CollaborationMode::PairProgramming(_) ModeKind::PairProgramming
)); );
} }
#[tokio::test] #[tokio::test]
+2 -7
View File
@@ -3,19 +3,14 @@ use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ModeKind;
fn mode_kind(mode: &CollaborationMode) -> ModeKind { fn mode_kind(mode: &CollaborationMode) -> ModeKind {
match mode { mode.mode
CollaborationMode::Plan(_) => ModeKind::Plan,
CollaborationMode::PairProgramming(_) => ModeKind::PairProgramming,
CollaborationMode::Execute(_) => ModeKind::Execute,
CollaborationMode::Custom(_) => ModeKind::Custom,
}
} }
pub(crate) fn default_mode(models_manager: &ModelsManager) -> Option<CollaborationMode> { pub(crate) fn default_mode(models_manager: &ModelsManager) -> Option<CollaborationMode> {
let presets = models_manager.list_collaboration_modes(); let presets = models_manager.list_collaboration_modes();
presets presets
.iter() .iter()
.find(|preset| matches!(preset, CollaborationMode::PairProgramming(_))) .find(|preset| preset.mode == ModeKind::PairProgramming)
.cloned() .cloned()
.or_else(|| presets.into_iter().next()) .or_else(|| presets.into_iter().next())
} }