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 f3a931f00..cb9ff3e18 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 @@ -1,7 +1,7 @@ //! Validates that the collaboration mode list endpoint returns the expected default presets. //! //! The test drives the app server through the MCP harness and asserts that the list response -//! includes the plan, pair programming, and execute modes with their default model and reasoning +//! includes the plan, coding, pair programming, and execute modes with their default model and reasoning //! effort settings, which keeps the API contract visible in one place. #![allow(clippy::unwrap_used)] @@ -45,7 +45,12 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> { let CollaborationModeListResponse { data: items } = to_response::(response)?; - let expected = vec![plan_preset(), pair_programming_preset(), execute_preset()]; + let expected = vec![ + plan_preset(), + code_preset(), + pair_programming_preset(), + execute_preset(), + ]; assert_eq!(expected, items); Ok(()) } @@ -74,6 +79,15 @@ fn pair_programming_preset() -> CollaborationMode { .unwrap() } +/// Builds the code preset that the list response is expected to return. +fn code_preset() -> CollaborationMode { + let presets = test_builtin_collaboration_mode_presets(); + presets + .into_iter() + .find(|p| p.mode == ModeKind::Code) + .unwrap() +} + /// Builds the execute preset that the list response is expected to return. /// /// The execute preset uses a different reasoning effort to capture the higher-effort diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 13cdbc674..68b9ba699 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -350,6 +350,7 @@ "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ "plan", + "code", "pair_programming", "execute", "custom" diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index a408f8ffc..9a9527aa7 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -105,7 +105,7 @@ pub enum Feature { Connectors, /// Steer feature flag - when enabled, Enter submits immediately instead of queuing. Steer, - /// Enable collaboration modes (Plan, Pair Programming, Execute). + /// Enable collaboration modes (Plan, Code, Pair Programming, Execute). CollaborationModes, /// Use the Responses API WebSocket transport for OpenAI by default. ResponsesWebsockets, 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 70989cb7c..ac1334c56 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -4,13 +4,19 @@ use codex_protocol::config_types::Settings; use codex_protocol::openai_models::ReasoningEffort; const COLLABORATION_MODE_PLAN: &str = include_str!("../../templates/collaboration_mode/plan.md"); +const COLLABORATION_MODE_CODE: &str = include_str!("../../templates/collaboration_mode/code.md"); const COLLABORATION_MODE_PAIR_PROGRAMMING: &str = include_str!("../../templates/collaboration_mode/pair_programming.md"); const COLLABORATION_MODE_EXECUTE: &str = include_str!("../../templates/collaboration_mode/execute.md"); pub(super) fn builtin_collaboration_mode_presets() -> Vec { - vec![plan_preset(), pair_programming_preset(), execute_preset()] + vec![ + plan_preset(), + code_preset(), + pair_programming_preset(), + execute_preset(), + ] } #[cfg(any(test, feature = "test-support"))] @@ -29,6 +35,17 @@ fn plan_preset() -> CollaborationMode { } } +fn code_preset() -> CollaborationMode { + CollaborationMode { + mode: ModeKind::Code, + settings: Settings { + model: "gpt-5.2-codex".to_string(), + reasoning_effort: Some(ReasoningEffort::Medium), + developer_instructions: Some(COLLABORATION_MODE_CODE.to_string()), + }, + } +} + fn pair_programming_preset() -> CollaborationMode { CollaborationMode { mode: ModeKind::PairProgramming, diff --git a/codex-rs/core/templates/collaboration_mode/code.md b/codex-rs/core/templates/collaboration_mode/code.md new file mode 100644 index 000000000..2294e6199 --- /dev/null +++ b/codex-rs/core/templates/collaboration_mode/code.md @@ -0,0 +1 @@ +you are now in code mode. diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index c242d07de..444901bf7 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -156,6 +156,7 @@ pub enum AltScreenMode { #[serde(rename_all = "snake_case")] pub enum ModeKind { Plan, + Code, PairProgramming, Execute, Custom, diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index ca83af901..16f4f22a9 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -49,6 +49,7 @@ pub(crate) struct FooterProps { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum CollaborationModeIndicator { Plan, + Code, PairProgramming, Execute, } @@ -64,6 +65,7 @@ impl CollaborationModeIndicator { }; match self { CollaborationModeIndicator::Plan => format!("Plan mode{suffix}"), + CollaborationModeIndicator::Code => format!("Code mode{suffix}"), CollaborationModeIndicator::PairProgramming => { format!("Pair Programming mode{suffix}") } @@ -75,6 +77,7 @@ impl CollaborationModeIndicator { let label = self.label(show_cycle_hint); match self { CollaborationModeIndicator::Plan => Span::from(label).magenta(), + CollaborationModeIndicator::Code => Span::from(label).cyan(), CollaborationModeIndicator::PairProgramming => Span::from(label).cyan(), CollaborationModeIndicator::Execute => Span::from(label).dim(), } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 89f6a96c5..1f7fad3ea 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -122,7 +122,7 @@ const DEFAULT_MODEL_DISPLAY_NAME: &str = "loading"; const PLAN_IMPLEMENTATION_TITLE: &str = "Implement this plan?"; const PLAN_IMPLEMENTATION_YES: &str = "Yes, implement this plan"; const PLAN_IMPLEMENTATION_NO: &str = "No, stay in Plan mode"; -const PLAN_IMPLEMENTATION_EXECUTE_MESSAGE: &str = "Implement the plan."; +const PLAN_IMPLEMENTATION_CODING_MESSAGE: &str = "Implement the plan."; use crate::app_event::AppEvent; use crate::app_event::ExitMode; @@ -932,10 +932,10 @@ impl ChatWidget { } fn open_plan_implementation_prompt(&mut self) { - let execute_mode = collaboration_modes::execute_mode(self.models_manager.as_ref()); - let (implement_actions, implement_disabled_reason) = match execute_mode { + let code_mode = collaboration_modes::code_mode(self.models_manager.as_ref()); + let (implement_actions, implement_disabled_reason) = match code_mode { Some(collaboration_mode) => { - let user_text = PLAN_IMPLEMENTATION_EXECUTE_MESSAGE.to_string(); + let user_text = PLAN_IMPLEMENTATION_CODING_MESSAGE.to_string(); let actions: Vec = vec![Box::new(move |tx| { tx.send(AppEvent::SubmitUserMessageWithMode { text: user_text.clone(), @@ -944,13 +944,13 @@ impl ChatWidget { })]; (actions, None) } - None => (Vec::new(), Some("Execute mode unavailable".to_string())), + None => (Vec::new(), Some("Code mode unavailable".to_string())), }; let items = vec![ SelectionItem { name: PLAN_IMPLEMENTATION_YES.to_string(), - description: Some("Switch to Execute and start coding.".to_string()), + description: Some("Switch to Code and start coding.".to_string()), selected_description: None, is_current: false, actions: implement_actions, @@ -3549,7 +3549,7 @@ impl ChatWidget { } pub(crate) fn open_collaboration_modes_popup(&mut self) { - let presets = self.models_manager.list_collaboration_modes(); + let presets = collaboration_modes::presets_for_tui(self.models_manager.as_ref()); if presets.is_empty() { self.add_info_message( "No collaboration modes are available right now.".to_string(), @@ -3563,6 +3563,7 @@ impl ChatWidget { .map(|preset| { let name = match preset.mode { ModeKind::Plan => "Plan", + ModeKind::Code => "Code", ModeKind::PairProgramming => "Pair Programming", ModeKind::Execute => "Execute", ModeKind::Custom => "Custom", @@ -4641,6 +4642,7 @@ impl ChatWidget { } match self.stored_collaboration_mode.mode { ModeKind::Plan => Some("Plan"), + ModeKind::Code => Some("Code"), ModeKind::PairProgramming => Some("Pair Programming"), ModeKind::Execute => Some("Execute"), ModeKind::Custom => None, @@ -4653,6 +4655,7 @@ impl ChatWidget { } match self.stored_collaboration_mode.mode { ModeKind::Plan => Some(CollaborationModeIndicator::Plan), + ModeKind::Code => Some(CollaborationModeIndicator::Code), ModeKind::PairProgramming => Some(CollaborationModeIndicator::PairProgramming), ModeKind::Execute => Some(CollaborationModeIndicator::Execute), ModeKind::Custom => None, @@ -4664,7 +4667,7 @@ impl ChatWidget { self.bottom_pane.set_collaboration_mode_indicator(indicator); } - /// Cycle to the next collaboration mode variant (Plan -> PairProgramming -> Execute -> Plan). + /// Cycle to the next collaboration mode variant (Plan -> Code -> Plan). fn cycle_collaboration_mode(&mut self) { if !self.collaboration_modes_enabled() { return; diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup.snap index da42eb07b..1fe6fe9e8 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup.snap @@ -4,7 +4,7 @@ expression: popup --- Implement this plan? -› 1. Yes, implement this plan Switch to Execute and start coding. +› 1. Yes, implement this plan Switch to Code and start coding. 2. No, stay in Plan mode Continue planning with the model. Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_no_selected.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_no_selected.snap index d5ba66c1b..417961658 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_no_selected.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_no_selected.snap @@ -4,7 +4,7 @@ expression: popup --- Implement this plan? - 1. Yes, implement this plan Switch to Execute and start coding. + 1. Yes, implement this plan Switch to Code and start coding. › 2. No, stay in Plan mode Continue planning with the model. Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 9fe7d3a1c..6a7401ce6 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1213,32 +1213,32 @@ async fn plan_implementation_popup_yes_emits_submit_message_event() { else { panic!("expected SubmitUserMessageWithMode, got {event:?}"); }; - assert_eq!(text, PLAN_IMPLEMENTATION_EXECUTE_MESSAGE); - assert_eq!(collaboration_mode.mode, ModeKind::Execute); + assert_eq!(text, PLAN_IMPLEMENTATION_CODING_MESSAGE); + assert_eq!(collaboration_mode.mode, ModeKind::Code); } #[tokio::test] -async fn submit_user_message_with_mode_sets_execute_collaboration_mode() { +async fn submit_user_message_with_mode_sets_coding_collaboration_mode() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5")).await; chat.thread_id = Some(ThreadId::new()); chat.set_feature_enabled(Feature::CollaborationModes, true); - let execute_mode = collaboration_modes::execute_mode(chat.models_manager.as_ref()) - .expect("expected execute collaboration mode"); - chat.submit_user_message_with_mode("Implement the plan.".to_string(), execute_mode); + let code_mode = collaboration_modes::code_mode(chat.models_manager.as_ref()) + .expect("expected code collaboration mode"); + chat.submit_user_message_with_mode("Implement the plan.".to_string(), code_mode); match next_submit_op(&mut op_rx) { Op::UserTurn { collaboration_mode: Some(CollaborationMode { - mode: ModeKind::Execute, + mode: ModeKind::Code, .. }), personality: None, .. } => {} other => { - panic!("expected Op::UserTurn with execute collab mode, got {other:?}") + panic!("expected Op::UserTurn with code collab mode, got {other:?}") } } } @@ -2229,10 +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_eq!(chat.stored_collaboration_mode.mode, ModeKind::Execute); + assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Plan); chat.handle_key_event(KeyEvent::from(KeyCode::BackTab)); - assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Plan); + assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Code); chat.on_task_started(); let before = chat.stored_collaboration_mode.clone(); @@ -2267,14 +2267,14 @@ async fn collab_slash_command_opens_picker_and_updates_mode() { Op::UserTurn { collaboration_mode: Some(CollaborationMode { - mode: ModeKind::PairProgramming, + mode: ModeKind::Code, .. }), personality: None, .. } => {} other => { - panic!("expected Op::UserTurn with pair programming collab mode, got {other:?}") + panic!("expected Op::UserTurn with code collab mode, got {other:?}") } } @@ -2285,20 +2285,20 @@ async fn collab_slash_command_opens_picker_and_updates_mode() { Op::UserTurn { collaboration_mode: Some(CollaborationMode { - mode: ModeKind::PairProgramming, + mode: ModeKind::Code, .. }), personality: None, .. } => {} other => { - panic!("expected Op::UserTurn with pair programming collab mode, got {other:?}") + panic!("expected Op::UserTurn with code collab mode, got {other:?}") } } } #[tokio::test] -async fn collab_mode_defaults_to_pair_programming_when_enabled() { +async fn collab_mode_defaults_to_coding_when_enabled() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; chat.thread_id = Some(ThreadId::new()); chat.set_feature_enabled(Feature::CollaborationModes, true); @@ -2310,26 +2310,23 @@ async fn collab_mode_defaults_to_pair_programming_when_enabled() { Op::UserTurn { collaboration_mode: Some(CollaborationMode { - mode: ModeKind::PairProgramming, + mode: ModeKind::Code, .. }), personality: None, .. } => {} other => { - panic!("expected Op::UserTurn with pair programming collab mode, got {other:?}") + panic!("expected Op::UserTurn with code collab mode, got {other:?}") } } } #[tokio::test] -async fn collab_mode_enabling_sets_pair_programming_default() { +async fn collab_mode_enabling_sets_coding_default() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; chat.set_feature_enabled(Feature::CollaborationModes, true); - assert_eq!( - chat.stored_collaboration_mode.mode, - ModeKind::PairProgramming - ); + assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Code); } #[tokio::test] diff --git a/codex-rs/tui/src/collaboration_modes.rs b/codex-rs/tui/src/collaboration_modes.rs index df034053d..c66010b57 100644 --- a/codex-rs/tui/src/collaboration_modes.rs +++ b/codex-rs/tui/src/collaboration_modes.rs @@ -6,11 +6,27 @@ fn mode_kind(mode: &CollaborationMode) -> ModeKind { mode.mode } +fn is_tui_mode(kind: ModeKind) -> bool { + matches!(kind, ModeKind::Plan | ModeKind::Code) +} + +fn filtered_presets(models_manager: &ModelsManager) -> Vec { + models_manager + .list_collaboration_modes() + .into_iter() + .filter(|preset| is_tui_mode(mode_kind(preset))) + .collect() +} + +pub(crate) fn presets_for_tui(models_manager: &ModelsManager) -> Vec { + filtered_presets(models_manager) +} + pub(crate) fn default_mode(models_manager: &ModelsManager) -> Option { - let presets = models_manager.list_collaboration_modes(); + let presets = filtered_presets(models_manager); presets .iter() - .find(|preset| preset.mode == ModeKind::PairProgramming) + .find(|preset| preset.mode == ModeKind::Code) .cloned() .or_else(|| presets.into_iter().next()) } @@ -19,7 +35,10 @@ pub(crate) fn mode_for_kind( models_manager: &ModelsManager, kind: ModeKind, ) -> Option { - let presets = models_manager.list_collaboration_modes(); + if !is_tui_mode(kind) { + return None; + } + let presets = filtered_presets(models_manager); presets.into_iter().find(|preset| mode_kind(preset) == kind) } @@ -32,7 +51,7 @@ pub(crate) fn next_mode( models_manager: &ModelsManager, current: &CollaborationMode, ) -> Option { - let presets = models_manager.list_collaboration_modes(); + let presets = filtered_presets(models_manager); if presets.is_empty() { return None; } @@ -44,9 +63,8 @@ pub(crate) fn next_mode( presets.get(next_index).cloned() } -pub(crate) fn execute_mode(models_manager: &ModelsManager) -> Option { - models_manager - .list_collaboration_modes() +pub(crate) fn code_mode(models_manager: &ModelsManager) -> Option { + filtered_presets(models_manager) .into_iter() - .find(|preset| mode_kind(preset) == ModeKind::Execute) + .find(|preset| mode_kind(preset) == ModeKind::Code) }