diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 621683923..48ee7d2d6 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -459,7 +459,7 @@ mod tests { #[test] fn collab_command_hidden_when_collaboration_modes_disabled() { let mut popup = CommandPopup::new(Vec::new(), CommandPopupFlags::default()); - popup.on_composer_text_change("/coll".to_string()); + popup.on_composer_text_change("/".to_string()); let cmds: Vec<&str> = popup .filtered_items() @@ -473,6 +473,10 @@ mod tests { !cmds.contains(&"collab"), "expected '/collab' to be hidden when collaboration modes are disabled, got {cmds:?}" ); + assert!( + !cmds.contains(&"plan"), + "expected '/plan' to be hidden when collaboration modes are disabled, got {cmds:?}" + ); } #[test] @@ -494,6 +498,25 @@ mod tests { } } + #[test] + fn plan_command_visible_when_collaboration_modes_enabled() { + let mut popup = CommandPopup::new( + Vec::new(), + CommandPopupFlags { + collaboration_modes_enabled: true, + connectors_enabled: false, + personality_command_enabled: true, + windows_degraded_sandbox_active: false, + }, + ); + popup.on_composer_text_change("/plan".to_string()); + + match popup.selected_item() { + Some(CommandItem::Builtin(cmd)) => assert_eq!(cmd.command(), "plan"), + other => panic!("expected plan to be selected for exact match, got {other:?}"), + } + } + #[test] fn personality_command_hidden_when_disabled() { let mut popup = CommandPopup::new( diff --git a/codex-rs/tui/src/bottom_pane/slash_commands.rs b/codex-rs/tui/src/bottom_pane/slash_commands.rs index 1efab04ec..34ad17330 100644 --- a/codex-rs/tui/src/bottom_pane/slash_commands.rs +++ b/codex-rs/tui/src/bottom_pane/slash_commands.rs @@ -18,7 +18,10 @@ pub(crate) fn builtins_for_input( built_in_slash_commands() .into_iter() .filter(|(_, cmd)| allow_elevate_sandbox || *cmd != SlashCommand::ElevateSandbox) - .filter(|(_, cmd)| collaboration_modes_enabled || *cmd != SlashCommand::Collab) + .filter(|(_, cmd)| { + collaboration_modes_enabled + || !matches!(*cmd, SlashCommand::Collab | SlashCommand::Plan) + }) .filter(|(_, cmd)| connectors_enabled || *cmd != SlashCommand::Apps) .filter(|(_, cmd)| personality_command_enabled || *cmd != SlashCommand::Personality) .collect() diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 61006d80c..74200e470 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2668,10 +2668,29 @@ impl ChatWidget { SlashCommand::Personality => { self.open_personality_popup(); } - SlashCommand::Collab => { - if self.collaboration_modes_enabled() { - self.open_collaboration_modes_popup(); + SlashCommand::Plan => { + if !self.collaboration_modes_enabled() { + self.add_info_message( + "Collaboration modes are disabled.".to_string(), + Some("Enable collaboration modes to use /plan.".to_string()), + ); + return; } + if let Some(mask) = collaboration_modes::plan_mask(self.models_manager.as_ref()) { + self.set_collaboration_mask(mask); + } else { + self.add_info_message("Plan mode unavailable right now.".to_string(), None); + } + } + SlashCommand::Collab => { + if !self.collaboration_modes_enabled() { + self.add_info_message( + "Collaboration modes are disabled.".to_string(), + Some("Enable collaboration modes to use /collab.".to_string()), + ); + return; + } + self.open_collaboration_modes_popup(); } SlashCommand::Agent => { self.app_event_tx.send(AppEvent::OpenAgentPicker); @@ -2844,11 +2863,9 @@ impl ChatWidget { let trimmed = args.trim(); match cmd { - SlashCommand::Collab => { + SlashCommand::Collab | SlashCommand::Plan => { let _ = trimmed; - if self.collaboration_modes_enabled() { - self.open_collaboration_modes_popup(); - } + self.dispatch_command(cmd); } SlashCommand::Review if !trimmed.is_empty() => { self.submit_op(Op::Review { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 9859e9e56..d8f22dc8e 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -2267,6 +2267,19 @@ async fn collab_slash_command_opens_picker_and_updates_mode() { } } +#[tokio::test] +async fn plan_slash_command_switches_to_plan_mode() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + chat.set_feature_enabled(Feature::CollaborationModes, true); + let initial = chat.current_collaboration_mode().clone(); + + chat.dispatch_command(SlashCommand::Plan); + + assert!(rx.try_recv().is_err(), "plan should not emit an app event"); + assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Plan); + assert_eq!(chat.current_collaboration_mode(), &initial); +} + #[tokio::test] async fn collaboration_modes_defaults_to_code_on_startup() { let codex_home = tempdir().expect("tempdir"); diff --git a/codex-rs/tui/src/collaboration_modes.rs b/codex-rs/tui/src/collaboration_modes.rs index 95908d8e9..e799ea9c5 100644 --- a/codex-rs/tui/src/collaboration_modes.rs +++ b/codex-rs/tui/src/collaboration_modes.rs @@ -59,3 +59,7 @@ pub(crate) fn next_mask( pub(crate) fn code_mask(models_manager: &ModelsManager) -> Option { mask_for_kind(models_manager, ModeKind::Code) } + +pub(crate) fn plan_mask(models_manager: &ModelsManager) -> Option { + mask_for_kind(models_manager, ModeKind::Plan) +} diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 9bab31461..9488577c1 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -25,6 +25,7 @@ pub enum SlashCommand { Fork, Init, Compact, + Plan, Collab, Agent, // Undo, @@ -63,6 +64,7 @@ impl SlashCommand { SlashCommand::Ps => "list background terminals", SlashCommand::Model => "choose what model and reasoning effort to use", SlashCommand::Personality => "choose a communication style for Codex", + SlashCommand::Plan => "switch to Plan mode", SlashCommand::Collab => "change collaboration mode (experimental)", SlashCommand::Agent => "switch the active agent thread", SlashCommand::Approvals => "choose what Codex can do without approval", @@ -112,6 +114,7 @@ impl SlashCommand { | SlashCommand::Exit => true, SlashCommand::Rollout => true, SlashCommand::TestApproval => true, + SlashCommand::Plan => true, SlashCommand::Collab => true, SlashCommand::Agent => true, }