Compare commits

...

1 Commits

Author SHA1 Message Date
Ahmed Ibrahim
81eb463fb8 Sync collaboration mode with /model 2026-01-19 13:31:38 -08:00
4 changed files with 214 additions and 7 deletions

View File

@@ -1,5 +1,6 @@
use crate::app_backtrack::BacktrackState;
use crate::app_event::AppEvent;
use crate::app_event::CollaborationModePreset;
use crate::app_event::ExitMode;
#[cfg(target_os = "windows")]
use crate::app_event::WindowsSandboxEnableMode;
@@ -48,6 +49,7 @@ use codex_core::protocol::SessionSource;
use codex_core::protocol::SkillErrorInfo;
use codex_core::protocol::TokenUsage;
use codex_protocol::ThreadId;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelUpgrade;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
@@ -980,6 +982,55 @@ impl App {
self.chat_widget.set_model(&model);
self.current_model = model;
}
AppEvent::OpenCollaborationModePopup => {
self.chat_widget.open_collaboration_mode_popup();
}
AppEvent::ApplyCollaborationModePreset(preset) => {
let collaboration_mode =
self.server
.list_collaboration_modes()
.into_iter()
.find(|mode| {
matches!(
(preset, mode),
(CollaborationModePreset::Plan, CollaborationMode::Plan(_))
| (
CollaborationModePreset::PairProgramming,
CollaborationMode::PairProgramming(_),
)
| (
CollaborationModePreset::Execute,
CollaborationMode::Execute(_)
)
)
});
let Some(collaboration_mode) = collaboration_mode else {
self.chat_widget.add_error_message(
"No collaboration modes are available right now.".to_string(),
);
return Ok(AppRunControl::Continue);
};
self.chat_widget
.set_collaboration_mode(collaboration_mode.clone());
let model = collaboration_mode.model().to_string();
let effort = collaboration_mode.reasoning_effort();
self.chat_widget.set_model(&model);
self.current_model = model;
self.on_update_reasoning_effort(effort);
self.chat_widget.submit_op(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
sandbox_policy: None,
model: None,
effort: None,
summary: None,
collaboration_mode: Some(collaboration_mode),
});
}
AppEvent::OpenReasoningPopup { model } => {
self.chat_widget.open_reasoning_popup(model);
}

View File

@@ -25,6 +25,13 @@ use codex_core::protocol::AskForApproval;
use codex_core::protocol::SandboxPolicy;
use codex_protocol::openai_models::ReasoningEffort;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CollaborationModePreset {
Plan,
PairProgramming,
Execute,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) enum WindowsSandboxEnableMode {
@@ -118,6 +125,12 @@ pub(crate) enum AppEvent {
models: Vec<ModelPreset>,
},
/// Open the collaboration mode picker.
OpenCollaborationModePopup,
/// Apply a collaboration mode preset for the current session.
ApplyCollaborationModePreset(CollaborationModePreset),
/// Open the confirmation prompt before enabling full access mode.
OpenFullAccessConfirmation {
preset: ApprovalPreset,

View File

@@ -92,6 +92,7 @@ use codex_core::skills::model::SkillMetadata;
use codex_protocol::ThreadId;
use codex_protocol::account::PlanType;
use codex_protocol::approvals::ElicitationRequestEvent;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::user_input::UserInput;
use crossterm::event::KeyCode;
@@ -115,6 +116,7 @@ use tracing::debug;
const DEFAULT_MODEL_DISPLAY_NAME: &str = "loading";
use crate::app_event::AppEvent;
use crate::app_event::CollaborationModePreset;
use crate::app_event::ExitMode;
#[cfg(target_os = "windows")]
use crate::app_event::WindowsSandboxEnableMode;
@@ -400,6 +402,7 @@ pub(crate) struct ChatWidget {
active_cell_revision: u64,
config: Config,
model: Option<String>,
collaboration_mode: Option<CollaborationMode>,
auth_manager: Arc<AuthManager>,
models_manager: Arc<ModelsManager>,
session_header: SessionHeader,
@@ -605,8 +608,11 @@ impl ChatWidget {
self.current_rollout_path = Some(event.rollout_path.clone());
let initial_messages = event.initial_messages.clone();
let model_for_header = event.model.clone();
let reasoning_effort = event.reasoning_effort;
self.model = Some(model_for_header.clone());
self.session_header.set_model(&model_for_header);
self.collaboration_mode = None;
self.set_reasoning_effort(reasoning_effort);
let session_info_cell = history_cell::new_session_info(
&self.config,
&model_for_header,
@@ -1673,6 +1679,7 @@ impl ChatWidget {
active_cell_revision: 0,
config,
model,
collaboration_mode: None,
auth_manager,
models_manager,
session_header: SessionHeader::new(model_for_header),
@@ -1772,6 +1779,7 @@ impl ChatWidget {
active_cell_revision: 0,
config,
model: Some(header_model.clone()),
collaboration_mode: None,
auth_manager,
models_manager,
session_header: SessionHeader::new(header_model),
@@ -2888,10 +2896,8 @@ impl ChatWidget {
let description =
(!preset.description.is_empty()).then_some(preset.description.clone());
let model = preset.model.clone();
let actions = Self::model_selection_actions(
model.clone(),
Some(preset.default_reasoning_effort),
);
let actions = self
.model_selection_actions(model.clone(), Some(preset.default_reasoning_effort));
SelectionItem {
name: preset.display_name.clone(),
description,
@@ -2904,6 +2910,28 @@ impl ChatWidget {
})
.collect();
if self.config.features.enabled(Feature::CollaborationModes) {
let current_mode = self
.collaboration_mode
.as_ref()
.map(Self::collaboration_mode_label)
.unwrap_or("default");
items.insert(
0,
SelectionItem {
name: "Collaboration mode".to_string(),
description: Some(format!(
"Choose how Codex collaborates (current: {current_mode})"
)),
actions: vec![Box::new(|tx| {
tx.send(AppEvent::OpenCollaborationModePopup);
})],
dismiss_on_select: true,
..Default::default()
},
);
}
if !other_presets.is_empty() {
let all_models = other_presets;
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
@@ -2939,6 +2967,80 @@ impl ChatWidget {
});
}
pub(crate) fn open_collaboration_mode_popup(&mut self) {
if !self.config.features.enabled(Feature::CollaborationModes) {
self.add_error_message("Collaboration modes are not enabled.".to_string());
return;
}
let current = self.collaboration_mode.as_ref();
let items = [
(
"Plan",
CollaborationModePreset::Plan,
"Plan first, then execute with user confirmation.",
),
(
"Pair programming",
CollaborationModePreset::PairProgramming,
"Collaborate like a coding partner; iterate quickly.",
),
(
"Execute",
CollaborationModePreset::Execute,
"Make changes directly; prioritize doing over discussion.",
),
]
.into_iter()
.map(|(name, preset, description)| {
let is_current = current.is_some_and(|mode| {
matches!(
(preset, mode),
(CollaborationModePreset::Plan, CollaborationMode::Plan(_))
| (
CollaborationModePreset::PairProgramming,
CollaborationMode::PairProgramming(_),
)
| (
CollaborationModePreset::Execute,
CollaborationMode::Execute(_)
)
)
});
SelectionItem {
name: name.to_string(),
description: Some(description.to_string()),
is_current,
actions: vec![Box::new(move |tx| {
tx.send(AppEvent::ApplyCollaborationModePreset(preset));
})],
dismiss_on_select: true,
..Default::default()
}
})
.collect();
let header = self.model_menu_header(
"Select Collaboration Mode",
"Choose how Codex should collaborate for upcoming turns.",
);
self.bottom_pane.show_selection_view(SelectionViewParams {
footer_hint: Some(standard_popup_hint_line()),
items,
header,
..Default::default()
});
}
fn collaboration_mode_label(mode: &CollaborationMode) -> &'static str {
match mode {
CollaborationMode::Plan(_) => "plan",
CollaborationMode::PairProgramming(_) => "pair programming",
CollaborationMode::Execute(_) => "execute",
CollaborationMode::Custom(_) => "custom",
}
}
fn is_auto_model(model: &str) -> bool {
model.starts_with("codex-auto-")
}
@@ -2962,6 +3064,24 @@ impl ChatWidget {
}
let mut items: Vec<SelectionItem> = Vec::new();
if self.config.features.enabled(Feature::CollaborationModes) {
let current_mode = self
.collaboration_mode
.as_ref()
.map(Self::collaboration_mode_label)
.unwrap_or("default");
items.push(SelectionItem {
name: "Collaboration mode".to_string(),
description: Some(format!(
"Choose how Codex collaborates (current: {current_mode})"
)),
actions: vec![Box::new(|tx| {
tx.send(AppEvent::OpenCollaborationModePopup);
})],
dismiss_on_select: true,
..Default::default()
});
}
for preset in presets.into_iter() {
let description =
(!preset.description.is_empty()).then_some(preset.description.to_string());
@@ -2998,9 +3118,17 @@ impl ChatWidget {
}
fn model_selection_actions(
&self,
model_for_action: String,
effort_for_action: Option<ReasoningEffortConfig>,
) -> Vec<SelectionAction> {
let collaboration_mode_for_op = self.collaboration_mode.as_ref().map(|mode| {
mode.with_updates(
Some(model_for_action.clone()),
Some(effort_for_action),
None,
)
});
vec![Box::new(move |tx| {
let effort_label = effort_for_action
.map(|effort| effort.to_string())
@@ -3012,7 +3140,7 @@ impl ChatWidget {
model: Some(model_for_action.clone()),
effort: Some(effort_for_action),
summary: None,
collaboration_mode: None,
collaboration_mode: collaboration_mode_for_op.clone(),
}));
tx.send(AppEvent::UpdateModel(model_for_action.clone()));
tx.send(AppEvent::UpdateReasoningEffort(effort_for_action));
@@ -3137,7 +3265,7 @@ impl ChatWidget {
};
let model_for_action = model_slug.clone();
let actions = Self::model_selection_actions(model_for_action, choice.stored);
let actions = self.model_selection_actions(model_for_action, choice.stored);
items.push(SelectionItem {
name: effort_label,
@@ -3176,6 +3304,10 @@ impl ChatWidget {
}
fn apply_model_and_effort(&self, model: String, effort: Option<ReasoningEffortConfig>) {
let collaboration_mode = self
.collaboration_mode
.as_ref()
.map(|mode| mode.with_updates(Some(model.clone()), Some(effort), None));
self.app_event_tx
.send(AppEvent::CodexOp(Op::OverrideTurnContext {
cwd: None,
@@ -3184,7 +3316,7 @@ impl ChatWidget {
model: Some(model.clone()),
effort: Some(effort),
summary: None,
collaboration_mode: None,
collaboration_mode,
}));
self.app_event_tx.send(AppEvent::UpdateModel(model.clone()));
self.app_event_tx
@@ -3922,12 +4054,22 @@ impl ChatWidget {
/// Set the reasoning effort in the widget's config copy.
pub(crate) fn set_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
self.config.model_reasoning_effort = effort;
if let Some(mode) = self.collaboration_mode.as_ref() {
self.collaboration_mode = Some(mode.with_updates(None, Some(effort), None));
}
}
/// Set the model in the widget's config copy.
pub(crate) fn set_model(&mut self, model: &str) {
self.session_header.set_model(model);
self.model = Some(model.to_string());
if let Some(mode) = self.collaboration_mode.as_ref() {
self.collaboration_mode = Some(mode.with_updates(Some(model.to_string()), None, None));
}
}
pub(crate) fn set_collaboration_mode(&mut self, collaboration_mode: CollaborationMode) {
self.collaboration_mode = Some(collaboration_mode);
}
fn current_model(&self) -> Option<&str> {

View File

@@ -404,6 +404,7 @@ async fn make_chatwidget_manual(
active_cell_revision: 0,
config: cfg,
model: Some(resolved_model.clone()),
collaboration_mode: None,
auth_manager: auth_manager.clone(),
models_manager: Arc::new(ModelsManager::new(codex_home, auth_manager)),
session_header: SessionHeader::new(resolved_model),