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

View File

@@ -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()
}

View File

@@ -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?;

View File

@@ -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::<ThreadStartResponse>(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 {

View File

@@ -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,

View File

@@ -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<CollaborationMode> {
}
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()),
},
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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<String> {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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<ReasoningEffort> {
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<String>,
effort: Option<Option<ReasoningEffort>>,
developer_instructions: Option<String>,
) -> 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,
}
}
}

View File

@@ -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<Self> {
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())

View File

@@ -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<SelectionItem> = 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<RateLimitSnapshot> {

View File

@@ -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]

View File

@@ -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<CollaborationMode> {
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())
}