diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 742a1c977..197109eae 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1201,6 +1201,15 @@ impl App { } } + let has_non_primary_agent_thread = self + .agent_picker_threads + .keys() + .any(|thread_id| Some(*thread_id) != self.primary_thread_id); + if !self.config.features.enabled(Feature::Collab) && !has_non_primary_agent_thread { + self.chat_widget.open_multi_agent_enable_prompt(); + return; + } + if self.agent_picker_threads.is_empty() { self.chat_widget .add_info_message("No agents available yet.".to_string(), None); @@ -3601,6 +3610,7 @@ mod tests { use crate::history_cell::HistoryCell; use crate::history_cell::UserHistoryCell; use crate::history_cell::new_session_info; + use assert_matches::assert_matches; use codex_core::CodexAuth; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; @@ -3966,6 +3976,51 @@ mod tests { Ok(()) } + #[tokio::test] + async fn open_agent_picker_prompts_to_enable_multi_agent_when_disabled() -> Result<()> { + let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await; + + app.open_agent_picker().await; + app.chat_widget + .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_matches!( + app_event_rx.try_recv(), + Ok(AppEvent::UpdateFeatureFlags { updates }) if updates == vec![(Feature::Collab, true)] + ); + let cell = match app_event_rx.try_recv() { + Ok(AppEvent::InsertHistoryCell(cell)) => cell, + other => panic!("expected InsertHistoryCell event, got {other:?}"), + }; + let rendered = cell + .display_lines(120) + .into_iter() + .map(|line| line.to_string()) + .collect::>() + .join("\n"); + assert!(rendered.contains("Multi-agent will be enabled in the next session.")); + Ok(()) + } + + #[tokio::test] + async fn open_agent_picker_allows_existing_agent_threads_when_feature_is_disabled() -> Result<()> + { + let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await; + let thread_id = ThreadId::new(); + app.thread_event_channels + .insert(thread_id, ThreadEventChannel::new(1)); + + app.open_agent_picker().await; + app.chat_widget + .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_matches!( + app_event_rx.try_recv(), + Ok(AppEvent::SelectAgentThread(selected_thread_id)) if selected_thread_id == thread_id + ); + Ok(()) + } + #[tokio::test] async fn refresh_pending_thread_approvals_only_lists_inactive_threads() { let mut app = make_test_app().await; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 78e8fcc66..4598c7ab8 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -164,6 +164,10 @@ 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_CODING_MESSAGE: &str = "Implement the plan."; +const MULTI_AGENT_ENABLE_TITLE: &str = "Enable multi-agent?"; +const MULTI_AGENT_ENABLE_YES: &str = "Yes, enable"; +const MULTI_AGENT_ENABLE_NO: &str = "Not now"; +const MULTI_AGENT_ENABLE_NOTICE: &str = "Multi-agent will be enabled in the next session."; const PLAN_MODE_REASONING_SCOPE_TITLE: &str = "Apply reasoning change"; const PLAN_MODE_REASONING_SCOPE_PLAN_ONLY: &str = "Apply to Plan mode override"; const PLAN_MODE_REASONING_SCOPE_ALL_MODES: &str = "Apply to global default and Plan mode override"; @@ -1568,6 +1572,41 @@ impl ChatWidget { }); } + pub(crate) fn open_multi_agent_enable_prompt(&mut self) { + let items = vec![ + SelectionItem { + name: MULTI_AGENT_ENABLE_YES.to_string(), + description: Some( + "Save the setting now. You will need a new session to use it.".to_string(), + ), + actions: vec![Box::new(|tx| { + tx.send(AppEvent::UpdateFeatureFlags { + updates: vec![(Feature::Collab, true)], + }); + tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::new_warning_event(MULTI_AGENT_ENABLE_NOTICE.to_string()), + ))); + })], + dismiss_on_select: true, + ..Default::default() + }, + SelectionItem { + name: MULTI_AGENT_ENABLE_NO.to_string(), + description: Some("Keep multi-agent disabled.".to_string()), + dismiss_on_select: true, + ..Default::default() + }, + ]; + + self.bottom_pane.show_selection_view(SelectionViewParams { + title: Some(MULTI_AGENT_ENABLE_TITLE.to_string()), + subtitle: Some("Multi-agent is currently disabled in your config.".to_string()), + footer_hint: Some(standard_popup_hint_line()), + items, + ..Default::default() + }); + } + pub(crate) fn set_token_info(&mut self, info: Option) { match info { Some(info) => self.apply_token_info(info), diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__multi_agent_enable_prompt.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__multi_agent_enable_prompt.snap new file mode 100644 index 000000000..ac25d9526 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__multi_agent_enable_prompt.snap @@ -0,0 +1,12 @@ +--- +source: tui/src/chatwidget/tests.rs +assertion_line: 6001 +expression: popup +--- + Enable multi-agent? + Multi-agent is currently disabled in your config. + +› 1. Yes, enable Save the setting now. You will need a new session to use it. + 2. Not now Keep multi-agent disabled. + + 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 d89ed4bf5..bfd7b4c44 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -5991,6 +5991,35 @@ async fn experimental_popup_shows_js_repl_node_requirement() { ); } +#[tokio::test] +async fn multi_agent_enable_prompt_snapshot() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; + + chat.open_multi_agent_enable_prompt(); + + let popup = render_bottom_popup(&chat, 80); + assert_snapshot!("multi_agent_enable_prompt", popup); +} + +#[tokio::test] +async fn multi_agent_enable_prompt_updates_feature_and_emits_notice() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + + chat.open_multi_agent_enable_prompt(); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + assert_matches!( + rx.try_recv(), + Ok(AppEvent::UpdateFeatureFlags { updates }) if updates == vec![(Feature::Collab, true)] + ); + let cell = match rx.try_recv() { + Ok(AppEvent::InsertHistoryCell(cell)) => cell, + other => panic!("expected InsertHistoryCell event, got {other:?}"), + }; + let rendered = lines_to_single_string(&cell.display_lines(120)); + assert!(rendered.contains("Multi-agent will be enabled in the next session.")); +} + #[tokio::test] async fn model_selection_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5-codex")).await;