From bf877af4180a40412794c64dad2ab49332c3def1 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 30 Oct 2026 03:50:12 +0000 Subject: [PATCH] Enable the agent command center in embedded mode (#58239) ## Why The agent command center previously required a shared background server, preventing embedded sessions from opening it and rejecting `codex agents --no-daemon`. ## What changed - Allow `codex agents --no-daemon` to use an embedded app server and open the command center without a shared server. - Route `codex agents` through the normal daemon startup, eligibility, and compatibility checks, including embedded fallback where applicable. - Return embedded sessions to the command center when canceling directory trust or archived-session confirmation. ## Testing Add CLI coverage for `--no-daemon` before and after `agents`, plus an integration test that verifies embedded mode is used even when a daemon is running. Update archived-session tests to expect cancellation to return to the command center. GitOrigin-RevId: 3bdf18a00456eae2e4d38103d12126e039d44119 --- codex-rs/cli/src/main.rs | 81 +++----------- codex-rs/cli/tests/app_server.rs | 4 +- codex-rs/cli/tests/daemon_startup.rs | 14 ++- codex-rs/cli/tests/doctor_path_safety.rs | 6 +- codex-rs/cli/tests/features.rs | 8 +- codex-rs/tui/src/app.rs | 3 - codex-rs/tui/src/app/agents_overview.rs | 100 ------------------ codex-rs/tui/src/app/agents_overview_tests.rs | 60 ----------- codex-rs/tui/src/app/event_dispatch.rs | 15 --- codex-rs/tui/src/app/exit_summary.rs | 13 +-- codex-rs/tui/src/app/startup.rs | 4 +- .../src/app/tests/backend_ownership_tests.rs | 1 - .../src/app/tests/history_hydration_tests.rs | 1 - codex-rs/tui/src/app_event.rs | 8 -- codex-rs/tui/src/cli.rs | 6 +- codex-rs/tui/src/daemon_startup.rs | 2 - codex-rs/tui/src/daemon_startup_tests.rs | 16 +-- codex-rs/tui/src/keymap.rs | 4 +- codex-rs/tui/src/keymap_setup/actions.rs | 2 +- codex-rs/tui/src/lib.rs | 27 +---- codex-rs/tui/src/main.rs | 5 +- codex-rs/tui/src/session_start.rs | 9 +- codex-rs/tui/src/session_start_tests.rs | 74 +++++-------- ...ew__tests__agents_daemon_start_failed.snap | 6 -- ...verview__tests__agents_daemon_started.snap | 6 -- ...view__tests__agents_overview_embedded.snap | 13 --- ...__tests__keymap_picker_all_tab_search.snap | 2 +- ...p__tests__keymap_picker_first_actions.snap | 2 +- codex-rs/tui/src/startup_orchestration.rs | 28 ++--- 29 files changed, 88 insertions(+), 432 deletions(-) delete mode 100644 codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_daemon_start_failed.snap delete mode 100644 codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_daemon_started.snap delete mode 100644 codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_embedded.snap diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 8a578c38a7..6c158f14b6 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -102,7 +102,6 @@ use codex_features::Stage; use codex_features::is_known_feature_key; use codex_home::CodexHomeUserInstructionsProvider; use codex_login::AuthManager; -use codex_login::is_workload_identity_selected; use codex_memories_write::clear_memory_roots_contents; use codex_models_manager::bundled_models_response; use codex_models_manager::manager::RefreshStrategy; @@ -144,7 +143,7 @@ struct MultitoolCli { #[derive(Debug, clap::Subcommand)] enum Subcommand { - /// Browse all agent sessions on the shared local app-server daemon. + /// Open the agent command center to browse and switch sessions. Agents(AgentsCommand), /// Internal: forward a local TCP socket through an HTTP/3 CONNECT proxy. @@ -337,8 +336,8 @@ struct DebugTraceReduceCommand { #[derive(Debug, Parser)] struct AgentsCommand { - /// The agents overview requires a shared server; this option is rejected. - #[arg(long, hide = true)] + /// Run the agents overview with an embedded app server. + #[arg(long)] no_daemon: bool, #[clap(flatten)] remote: InteractiveRemoteOptions, @@ -771,10 +770,7 @@ fn handle_app_exit( eprintln!("ERROR: {message}"); true } - ExitReason::UserRequested - | ExitReason::Archived(_) - | ExitReason::TurnInterrupted - | ExitReason::ThreadRemoved => false, + ExitReason::UserRequested | ExitReason::TurnInterrupted => false, }; let update_action = exit_info.update_action; @@ -1131,19 +1127,6 @@ async fn cli_main( "`codex agents` cannot apply local provider or additional-directory overrides to a remote server" ); } - if is_workload_identity_selected() { - anyhow::bail!( - "`codex agents` is unavailable while workload identity is active" - ); - } - if root_remote.is_none() { - resolve_remote_endpoint( - /*remote*/ None, - root_remote_auth_token_env.clone(), - )?; - #[cfg(not(any(unix, windows)))] - anyhow::bail!("`codex agents` requires `--remote` on this platform"); - } interactive.agents_overview = true; } let exit_info = run_interactive_tui( @@ -2500,17 +2483,10 @@ async fn run_interactive_tui( remote_auth_token_env: Option, arg0_paths: Arg0DispatchPaths, ) -> std::io::Result { - if interactive.no_daemon { - if interactive.agents_overview { - return Ok(AppExitInfo::fatal( - "--no-daemon cannot be used with codex agents. The agents overview requires a shared server. Use codex --no-daemon to work without it.", - )); - } - if remote.is_some() { - return Ok(AppExitInfo::fatal( - "--no-daemon cannot be used with --remote.", - )); - } + if interactive.no_daemon && remote.is_some() { + return Ok(AppExitInfo::fatal( + "--no-daemon cannot be used with --remote.", + )); } if let Some(prompt) = interactive.prompt.take() { // Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state. @@ -2535,25 +2511,6 @@ async fn run_interactive_tui( } } - #[cfg(any(unix, windows))] - if interactive.agents_overview && remote.is_none() { - if !std::io::stdin().is_terminal() { - return Ok(AppExitInfo::fatal("stdin is not a terminal")); - } - if !std::io::stdout().is_terminal() { - return Ok(AppExitInfo::fatal("stdout is not a terminal")); - } - cloud_config::load_config(&interactive.config_overrides, LoaderOverrides::default()) - .await - .map_err(std::io::Error::other)?; - let output = codex_app_server_daemon::run(AppServerLifecycleCommand::Start) - .await - .map_err(|err| std::io::Error::other(format!( - "{err:#}\nThe agents overview requires a shared server. Use codex --no-daemon to work without it." - )))?; - interactive.managed_daemon = output.backend.is_some(); - } - let remote_endpoint = match resolve_remote_endpoint(remote, remote_auth_token_env.clone()) { Ok(remote_endpoint) => remote_endpoint, Err(err) if is_remote_auth_usage_error(&err) => { @@ -2561,6 +2518,14 @@ async fn run_interactive_tui( } Err(err) => return Err(err), }; + if interactive.agents_overview && remote_endpoint.is_none() { + if !std::io::stdin().is_terminal() { + return Ok(AppExitInfo::fatal("stdin is not a terminal")); + } + if !std::io::stdout().is_terminal() { + return Ok(AppExitInfo::fatal("stdout is not a terminal")); + } + } let start_tui = || { codex_tui::run_main( interactive.clone(), @@ -3902,20 +3867,6 @@ enabled = true insta::assert_snapshot!(lines.join("\n"), @"Session ID: 123e4567-e89b-12d3-a456-426614174000"); } - #[test] - fn format_exit_messages_confirms_archive() { - let mut exit_info = sample_exit_info( - Some("123e4567-e89b-12d3-a456-426614174000"), - /*thread_name*/ None, - ); - exit_info.exit_reason = ExitReason::Archived(exit_info.thread_id.unwrap()); - let lines = exit_info.format_exit_messages(/*color_enabled*/ false); - insta::assert_snapshot!(lines.join("\n"), @" - Token usage: total=2 input=0 output=2 - Session archived: 123e4567-e89b-12d3-a456-426614174000 - "); - } - #[test] fn format_exit_messages_includes_session_id_for_fatal_exit_without_resume_hint() { let exit_info = AppExitInfo { diff --git a/codex-rs/cli/tests/app_server.rs b/codex-rs/cli/tests/app_server.rs index ba5c01b6c4..3fab9e05a4 100644 --- a/codex-rs/cli/tests/app_server.rs +++ b/codex-rs/cli/tests/app_server.rs @@ -64,6 +64,8 @@ fn agents_accept_interactive_configuration_overrides() -> Result<()> { ["-c", "features.multi_agent_mode=true", "agents"].as_slice(), ["--enable", "multi_agent_mode", "agents"].as_slice(), ["--yolo", "agents"].as_slice(), + ["--no-daemon", "agents"].as_slice(), + ["agents", "--no-daemon"].as_slice(), ["--search", "agents"].as_slice(), ["--model", "gpt-5", "agents"].as_slice(), ["--approve-for-me", "agents"].as_slice(), @@ -71,8 +73,6 @@ fn agents_accept_interactive_configuration_overrides() -> Result<()> { ] { let mut cmd = codex_command(codex_home.path())?; cmd.env("TERM", "xterm-256color").args(args); - #[cfg(not(any(unix, windows)))] - cmd.args(["--remote", "ws://127.0.0.1:4512"]); cmd.assert() .failure() diff --git a/codex-rs/cli/tests/daemon_startup.rs b/codex-rs/cli/tests/daemon_startup.rs index ae2fe3eefc..2cb900d239 100644 --- a/codex-rs/cli/tests/daemon_startup.rs +++ b/codex-rs/cli/tests/daemon_startup.rs @@ -20,6 +20,12 @@ async fn agents_can_offer_restart_for_managed_daemon() -> Result<()> { daemon_startup("agents").await } +#[tokio::test] +#[cfg(unix)] +async fn agents_no_daemon_uses_embedded_even_with_a_running_daemon() -> Result<()> { + daemon_startup("agents-no-daemon").await +} + #[tokio::test] #[cfg(unix)] async fn agents_defers_launch_directory_trust_until_starting_a_task() -> Result<()> { @@ -236,6 +242,7 @@ async fn daemon_startup(command: &str) -> Result<()> { command, "start" | "agents" + | "agents-no-daemon" | "agents-untrusted" | "agents-untrusted-new" | "discovered" @@ -269,7 +276,7 @@ async fn daemon_startup(command: &str) -> Result<()> { } let pid_file = home.path().join("app-server-daemon/daemon.pid"); let result = async { - let mut existing_daemon = if matches!(command, "bedrock-running" | "discovered") + let mut existing_daemon = if matches!(command, "bedrock-running" | "discovered" | "agents-no-daemon") || (mismatch && !disabling) { let started = Command::new(&codex) @@ -291,6 +298,11 @@ async fn daemon_startup(command: &str) -> Result<()> { // The draft header is visible before the session's command composer is ready. steps.push_back(("GPT-5.6-Terra", b"/status\r")); "Server:Localbackgroundserver" + } else if command == "agents-no-daemon" { + args.extend(["agents".into(), "--no-daemon".into()]); + steps.push_back(("Notasksyet", b"n")); + steps.push_back(("GPT-5.6-Terra", b"/daemon\r")); + "Notconnectedtothelocalbackgroundserver" } else if command == "agents" { args.push("agents".into()); steps.push_back(("Notasksyet", b"\x1b")); diff --git a/codex-rs/cli/tests/doctor_path_safety.rs b/codex-rs/cli/tests/doctor_path_safety.rs index 66d6d2c0cf..015fb063a2 100644 --- a/codex-rs/cli/tests/doctor_path_safety.rs +++ b/codex-rs/cli/tests/doctor_path_safety.rs @@ -579,8 +579,8 @@ async fn interactive_tmux_startup_does_not_execute_workspace_helpers() -> Result output.chars().take(/*n*/ 4000).collect::() ) })??; - // The trust screen discards pending input after its first draw. Retry the quit key - // until that drain has finished, and keep consuming output during normal shutdown. + // The first Ctrl+C leaves the untrusted workspace for the command center. + // Keep sending Ctrl+C so the TUI exits even if the first draw discarded input. let mut quit_retry = tokio::time::interval(Duration::from_millis(/*millis*/ 250)); let mut exit_rx = spawned.exit_rx; timeout(Duration::from_secs(/*secs*/ 10), async { @@ -588,7 +588,7 @@ async fn interactive_tmux_startup_does_not_execute_workspace_helpers() -> Result tokio::select! { result = &mut exit_rx => return result, _ = quit_retry.tick() => { - let _ = writer.send(b"2".to_vec()).await; + let _ = writer.send(b"\x03".to_vec()).await; } Some(_) = output_rx.recv() => {} } diff --git a/codex-rs/cli/tests/features.rs b/codex-rs/cli/tests/features.rs index 6da9fd9325..88f5ad6079 100644 --- a/codex-rs/cli/tests/features.rs +++ b/codex-rs/cli/tests/features.rs @@ -400,10 +400,8 @@ fn remote_start_allows_network_access_overrides_before_requiring_terminal() -> R } #[test] -fn no_daemon_rejects_agents_and_explicit_remote_targets() -> Result<()> { +fn no_daemon_rejects_queue_and_explicit_remote_targets() -> Result<()> { for args in [ - "--no-daemon agents", - "agents --no-daemon", "--no-daemon queue --thread example --message hello", "--no-daemon --remote ws://localhost:9999 agents", "--no-daemon --remote ws://localhost:9999", @@ -416,9 +414,7 @@ fn no_daemon_rejects_agents_and_explicit_remote_targets() -> Result<()> { ] { let args = args.split_whitespace().collect::>(); let home = TempDir::new()?; - let expected = if args.contains(&"agents") { - "--no-daemon cannot be used with codex agents." - } else if args.contains(&"queue") && !args.contains(&"--remote") { + let expected = if args.contains(&"queue") && !args.contains(&"--remote") { "--no-daemon cannot be used with codex queue." } else { "--no-daemon cannot be used with --remote." diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index fbc109028f..205674fbb1 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -496,10 +496,7 @@ pub(crate) enum AppRunControl { #[derive(Debug, Clone)] pub enum ExitReason { UserRequested, - Archived(ThreadId), TurnInterrupted, - /// The current thread was deleted, rather than disconnected. - ThreadRemoved, Fatal(String), } diff --git a/codex-rs/tui/src/app/agents_overview.rs b/codex-rs/tui/src/app/agents_overview.rs index 6c99b452b4..79fcc543cd 100644 --- a/codex-rs/tui/src/app/agents_overview.rs +++ b/codex-rs/tui/src/app/agents_overview.rs @@ -23,7 +23,6 @@ use super::session_lifecycle::ThreadAttachPresentation; use super::*; use crate::app_event::AgentsOverviewThreadRefresh; use crate::bottom_pane::BottomPaneView; -use crate::bottom_pane::SelectionDescriptionLayout; use crate::bottom_pane::SelectionItem; use crate::bottom_pane::SelectionViewParams; use crate::chatwidget::ThreadInputStateRestoreMode; @@ -89,58 +88,6 @@ impl Drop for AgentsOverviewState { } impl App { - pub(super) fn open_initial_agents_overview(&mut self, app_server: &AppServerSession) { - if !self.app_server_target.supports_command_center() { - let workload_identity_selected = codex_login::is_workload_identity_selected(); - self.chat_widget.show_selection_view(SelectionViewParams { - title: Some("Shared agents unavailable".to_string()), - subtitle: Some( - if workload_identity_selected { - "The agents dashboard is unavailable while workload identity is active." - } else if cfg!(any(unix, windows)) { - "This session isn’t connected to a shared background server." - } else { - "Connect to a remote background server to use the agents dashboard." - } - .to_string(), - ), - footer_note: (cfg!(any(unix, windows)) && !workload_identity_selected).then(|| { - Line::from( - "Starting a background server will not interrupt or move this session." - .dim(), - ) - }), - items: [ - #[cfg(any(unix, windows))] - (!workload_identity_selected).then(|| SelectionItem { - name: "Start background server".to_string(), - description: Some( - "Open `codex agents` in another terminal afterward".to_string(), - ), - actions: vec![Box::new(|tx| tx.send(AppEvent::StartAgentsDaemon))], - dismiss_on_select: true, - ..Default::default() - }), - Some(SelectionItem { - name: "Return to this session".to_string(), - dismiss_on_select: true, - ..Default::default() - }), - ] - .into_iter() - .flatten() - .collect(), - description_layout: SelectionDescriptionLayout::HideWhenNarrow { - min_description_width: 28, - }, - ..SelectionViewParams::picker() - }); - return; - } - - self.open_agents_overview(app_server); - } - pub(super) fn open_agents_overview(&mut self, app_server: &AppServerSession) { let threads = self .agents_overview @@ -1198,53 +1145,6 @@ impl App { self.refresh_agents_overview_threads(app_server); } } - - #[cfg(any(unix, windows))] - pub(super) fn start_agents_daemon(&self) { - let app_event_tx = self.app_event_tx.clone(); - tokio::spawn(async move { - let result = async { - let current_executable = - std::env::current_exe().map_err(|error| error.to_string())?; - let executable = if current_executable - .file_stem() - .and_then(std::ffi::OsStr::to_str) - .is_some_and(|name| { - if cfg!(windows) { - name.eq_ignore_ascii_case("codex-tui") - } else { - name == "codex-tui" - } - }) { - current_executable.with_file_name(if cfg!(windows) { - "codex.exe" - } else { - "codex" - }) - } else { - current_executable - }; - let output = tokio::process::Command::new(executable) - .args(["app-server", "daemon", "start"]) - .output() - .await - .map_err(|error| error.to_string())?; - if output.status.success() { - Ok(()) - } else { - let message = String::from_utf8_lossy(&output.stderr).trim().to_string(); - Err(if message.is_empty() { - format!("daemon process exited with {}", output.status) - } else { - message - }) - } - } - .await; - - app_event_tx.send(AppEvent::AgentsDaemonStarted { result }); - }); - } } #[cfg(test)] diff --git a/codex-rs/tui/src/app/agents_overview_tests.rs b/codex-rs/tui/src/app/agents_overview_tests.rs index a1632f2716..562271c9b5 100644 --- a/codex-rs/tui/src/app/agents_overview_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_tests.rs @@ -1511,66 +1511,6 @@ async fn shared_overview_shows_only_root_sessions() { assert!(app.last_rendered_history_tail.is_some()); } -#[cfg(any(unix, windows))] -#[tokio::test] -async fn embedded_sessions_offer_to_start_a_background_server_without_migrating() { - let mut app = make_test_app().await; - let app_server = crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref()) - .await - .expect("embedded app server"); - - app.open_initial_agents_overview(&app_server); - - insta::with_settings!({snapshot_path => "../snapshots"}, { - insta::assert_snapshot!( - "agents_overview_embedded", - render_bottom_popup(&app.chat_widget, /*width*/ 96) - ); - }); - app_server.shutdown().await.expect("shutdown app server"); -} - -#[cfg(any(unix, windows))] -#[tokio::test] -async fn daemon_start_result_snapshots() -> Result<()> { - let (mut app, mut app_event_rx, _op_rx) = - crate::app::tests::make_test_app_with_channels().await; - let mut app_server = - crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref()).await?; - let mut tui = crate::tui::test_support::make_test_tui()?; - - for (name, result) in [ - ("agents_daemon_started", Ok(())), - ( - "agents_daemon_start_failed", - Err("The host does not allow detached processes".to_string()), - ), - ] { - app.handle_event( - &mut tui, - &mut app_server, - AppEvent::AgentsDaemonStarted { result }, - ) - .await?; - let cell = match app_event_rx.try_recv()? { - AppEvent::InsertHistoryCell(cell) => cell, - other => panic!("expected daemon result history, got {other:?}"), - }; - let rendered = cell - .display_lines(/*width*/ 80) - .iter() - .map(ToString::to_string) - .collect::>() - .join("\n"); - insta::with_settings!({snapshot_path => "../snapshots"}, { - insta::assert_snapshot!(name, rendered); - }); - } - - app_server.shutdown().await?; - Ok(()) -} - #[tokio::test] async fn filtered_dashboard_actions_use_configured_shortcuts() { let mut app = make_test_app().await; diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 32ab400b6b..27fd797c1e 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -3157,21 +3157,6 @@ impl App { self.stop_agents_overview_thread(app_server, thread_id) .await; } - #[cfg(any(unix, windows))] - AppEvent::StartAgentsDaemon => { - self.start_agents_daemon(); - } - #[cfg(any(unix, windows))] - AppEvent::AgentsDaemonStarted { result } => match result { - Ok(()) => self.chat_widget.add_info_message( - "Background server started. Run `codex agents` in another terminal; this session remains unchanged." - .to_string(), - /*hint*/ None, - ), - Err(error) => self - .chat_widget - .add_error_message(format!("Failed to start the background server: {error}")), - }, AppEvent::OpenAgentPicker => { self.open_agent_picker(app_server).await; } diff --git a/codex-rs/tui/src/app/exit_summary.rs b/codex-rs/tui/src/app/exit_summary.rs index f9932ced06..4cc843d9b4 100644 --- a/codex-rs/tui/src/app/exit_summary.rs +++ b/codex-rs/tui/src/app/exit_summary.rs @@ -23,12 +23,7 @@ pub struct DisconnectInfo { impl App { pub(super) fn exit_info(&self, exit_reason: ExitReason) -> AppExitInfo { - let thread_id = match exit_reason { - ExitReason::Archived(_) | ExitReason::ThreadRemoved => None, - ExitReason::UserRequested | ExitReason::TurnInterrupted | ExitReason::Fatal(_) => { - self.chat_widget.thread_id().or(self.primary_thread_id) - } - }; + let thread_id = self.chat_widget.thread_id().or(self.primary_thread_id); let disconnect_info = thread_id.and_then(|_| { let command = match &self.app_server_target { AppServerTarget::Embedded => return None, @@ -93,7 +88,7 @@ impl AppExitInfo { { let turn_interrupted = matches!(self.exit_reason, ExitReason::TurnInterrupted); let message = match self.exit_reason { - ExitReason::UserRequested | ExitReason::Archived(_) | ExitReason::ThreadRemoved => { + ExitReason::UserRequested => { "Disconnected from this task. Any running work continues." } ExitReason::Fatal(_) => "Disconnected from this task. Work may still be running.", @@ -132,9 +127,7 @@ impl AppExitInfo { if !self.token_usage.is_zero() { lines.push(self.token_usage.to_string()); } - if let ExitReason::Archived(thread_id) = self.exit_reason { - lines.push(format!("Session archived: {thread_id}")); - } else if let Some(thread) = self.resume_hint { + if let Some(thread) = self.resume_hint { lines.push("To continue this session, run:".to_string()); lines.push(format!( " {}", diff --git a/codex-rs/tui/src/app/startup.rs b/codex-rs/tui/src/app/startup.rs index 5e688ca5a1..fe37a84c62 100644 --- a/codex-rs/tui/src/app/startup.rs +++ b/codex-rs/tui/src/app/startup.rs @@ -743,7 +743,6 @@ impl App { config: &config, local_settings: &local_settings, }, - &app_server_target, &target_session, action, resumed, @@ -848,7 +847,6 @@ impl App { config: &config, local_settings: &local_settings, }, - &app_server_target, &target_session, action, forked, @@ -1111,7 +1109,7 @@ See the Codex keymap documentation for supported actions and examples." ); } if start_in_agents_overview { - app.open_initial_agents_overview(&app_server); + app.open_agents_overview(&app_server); } else { app.refresh_agents_overview_threads(&app_server); } diff --git a/codex-rs/tui/src/app/tests/backend_ownership_tests.rs b/codex-rs/tui/src/app/tests/backend_ownership_tests.rs index fa8a481499..92f22bfe89 100644 --- a/codex-rs/tui/src/app/tests/backend_ownership_tests.rs +++ b/codex-rs/tui/src/app/tests/backend_ownership_tests.rs @@ -77,7 +77,6 @@ async fn foreign_backend_selection_cannot_resume_same_id_or_unarchive() -> Resul config: &app.config, local_settings: &app.local_settings, }, - &AppServerTarget::Embedded, &target, action, Err(color_eyre::eyre::eyre!( diff --git a/codex-rs/tui/src/app/tests/history_hydration_tests.rs b/codex-rs/tui/src/app/tests/history_hydration_tests.rs index ecc4ba35e4..111f813bc3 100644 --- a/codex-rs/tui/src/app/tests/history_hydration_tests.rs +++ b/codex-rs/tui/src/app/tests/history_hydration_tests.rs @@ -300,7 +300,6 @@ async fn history_hydration_archived_retry_uses_first_attempt_runtime_settings() config: &app.config, local_settings: &app.local_settings, }, - &AppServerTarget::Embedded, &target, action, initial, diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 298f59f816..94f1ee31d2 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -399,14 +399,6 @@ pub(crate) enum AppEvent { thread_id: ThreadId, action: AgentsOverviewAction, }, - /// Start the shared app-server daemon without moving the current embedded session. - #[cfg(any(unix, windows))] - StartAgentsDaemon, - /// Report whether starting the shared app-server daemon succeeded. - #[cfg(any(unix, windows))] - AgentsDaemonStarted { - result: Result<(), String>, - }, /// Open the agent picker for switching active threads. OpenAgentPicker, /// Merge a completed root-scoped agent-picker refresh without blocking terminal input. diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index 29469b295e..ca006e91b1 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -42,14 +42,10 @@ pub struct Cli { #[clap(skip)] pub resume_include_non_interactive: bool, - /// Internal: open the daemon-wide agents overview instead of starting a thread. + /// Internal: open the agents overview instead of starting a thread. #[clap(skip)] pub agents_overview: bool, - /// Internal: the launching CLI verified that the local daemon has a managed backend. - #[clap(skip)] - pub managed_daemon: bool, - // Internal controls set by the top-level `codex fork` subcommand. // These are not exposed as user flags on the base `codex` command. #[clap(skip)] diff --git a/codex-rs/tui/src/daemon_startup.rs b/codex-rs/tui/src/daemon_startup.rs index 6c7dca5074..3ca2317448 100644 --- a/codex-rs/tui/src/daemon_startup.rs +++ b/codex-rs/tui/src/daemon_startup.rs @@ -73,8 +73,6 @@ pub(super) fn exclusion( Some("workload identity") } else if exec_server_url.is_some() { Some("executor selection (CODEX_EXEC_SERVER_URL)") - } else if cli.agents_overview { - None } else if cli.config_profile_v2.is_some() { Some("--profile") } else { diff --git a/codex-rs/tui/src/daemon_startup_tests.rs b/codex-rs/tui/src/daemon_startup_tests.rs index c689eeb509..1fa6aedea4 100644 --- a/codex-rs/tui/src/daemon_startup_tests.rs +++ b/codex-rs/tui/src/daemon_startup_tests.rs @@ -367,8 +367,8 @@ fn daemon_eligibility_preserves_launch_options_and_explains_exclusions() { expected ); } - let mut cli = Cli::parse_from(["codex"]); - let overrides = vec![("web_search".into(), toml::Value::String("live".into()))]; + let cli = Cli::parse_from(["codex"]); + let overrides = [("web_search".into(), toml::Value::String("live".into()))]; let loader = LoaderOverrides { ignore_user_config: true, ..Default::default() @@ -402,18 +402,6 @@ fn daemon_eligibility_preserves_launch_options_and_explains_exclusions() { Some(expected) ); } - cli.agents_overview = true; - cli.strict_config = true; - assert_eq!( - daemon_startup::exclusion( - &cli, - &overrides, - &LoaderOverrides::default(), - /*workload_identity_selected*/ false, - /*exec_server_url*/ None - ), - None - ); } #[test] diff --git a/codex-rs/tui/src/keymap.rs b/codex-rs/tui/src/keymap.rs index f9b7c52bff..442213036c 100644 --- a/codex-rs/tui/src/keymap.rs +++ b/codex-rs/tui/src/keymap.rs @@ -102,7 +102,7 @@ pub(crate) struct RuntimeKeymap { #[derive(Clone, Debug)] pub(crate) struct AppKeymap { - /// Open the daemon-wide agent-session overview. + /// Open the agent command center. pub(crate) open_agents: Vec, /// Open transcript overlay. pub(crate) open_transcript: Vec, @@ -452,7 +452,7 @@ impl ListKeymap { } } -/// Task-management shortcuts specific to the shared agents dashboard. +/// Task-management shortcuts specific to the agents dashboard. #[derive(Clone, Debug)] pub(crate) struct AgentsKeymap { pub(crate) resume: Vec, diff --git a/codex-rs/tui/src/keymap_setup/actions.rs b/codex-rs/tui/src/keymap_setup/actions.rs index 4251983461..a564c377b1 100644 --- a/codex-rs/tui/src/keymap_setup/actions.rs +++ b/codex-rs/tui/src/keymap_setup/actions.rs @@ -111,7 +111,7 @@ macro_rules! define_keymap_actions { define_keymap_actions! { global "Global" { - open_agents => "Open the shared agent-session overview.", + open_agents => "Open the agent command center.", open_transcript => "Open the transcript overlay.", find_transcript => "Find text in the full transcript.", // Keep the new warnings action out of shared-config writes until older strict readers diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 0123b5ccef..50b07118cd 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -338,13 +338,6 @@ pub(crate) enum AppServerTarget { } impl AppServerTarget { - fn supports_command_center(&self) -> bool { - match self { - Self::Embedded => false, - Self::LocalDaemon { .. } | Self::Remote { .. } => true, - } - } - fn preserve_selected_backend(&mut self, selection: &resume_picker::SessionSelection) { if matches!( selection, @@ -1955,12 +1948,10 @@ async fn run_ratatui_app( } // The shared overview does not open a workspace; session selection checks trust when needed. - // Embedded fallback cannot show the overview and still needs normal startup initialization. - if (!matches!( + if !matches!( session_selection, resume_picker::SessionSelection::AgentsOverview - ) || !app_server_target.supports_command_center()) - && (!uses_remote_workspace || remote_cwd_override.is_some()) + ) && (!uses_remote_workspace || remote_cwd_override.is_some()) { let resumed_thread = if matches!(app_server_target, AppServerTarget::LocalDaemon { .. }) && let resume_picker::SessionSelection::Resume(target) = &session_selection @@ -1987,8 +1978,7 @@ async fn run_ratatui_app( trust_cwd, onboarding::DirectoryTrustOptions { resumed_thread: resumed_thread.as_ref(), - cancel: (!app_server_target.supports_command_center()) - .then_some(onboarding::TrustCancelAction::Quit), + ..Default::default() }, Some(&mut startup_draft), ) @@ -2040,17 +2030,6 @@ async fn run_ratatui_app( trust_decision_was_made = true; } } else if consent.should_exit { - if !app_server_target.supports_command_center() { - shutdown_startup_session(Some(app_server), &mut terminal_restore_guard).await; - return Ok(AppExitInfo { - token_usage: crate::token_usage::TokenUsage::default(), - thread_id: None, - resume_hint: None, - disconnect_info: None, - update_action: None, - exit_reason: ExitReason::UserRequested, - }); - } if !uses_remote_workspace { config = load_config_or_exit_with_fallback_cwd( cli_kv_overrides.clone(), diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs index 789c7f15f2..1e5c8d2513 100644 --- a/codex-rs/tui/src/main.rs +++ b/codex-rs/tui/src/main.rs @@ -43,10 +43,7 @@ fn main() -> anyhow::Result<()> { eprintln!("ERROR: {message}"); true } - ExitReason::UserRequested - | ExitReason::Archived(_) - | ExitReason::TurnInterrupted - | ExitReason::ThreadRemoved => false, + ExitReason::UserRequested | ExitReason::TurnInterrupted => false, }; let color_enabled = supports_color::on(Stream::Stdout).is_some(); diff --git a/codex-rs/tui/src/session_start.rs b/codex-rs/tui/src/session_start.rs index ae71976bbd..5ea631357e 100644 --- a/codex-rs/tui/src/session_start.rs +++ b/codex-rs/tui/src/session_start.rs @@ -82,7 +82,6 @@ pub(crate) struct SessionStartConfig<'a> { pub(crate) async fn complete_session_start( app_server: &mut AppServerSession, config: SessionStartConfig<'_>, - app_server_target: &crate::AppServerTarget, target: &SessionTarget, action: SessionStartAction, initial_result: Result, @@ -104,13 +103,7 @@ pub(crate) async fn complete_session_start( } match confirm().await? { - UnarchiveChoice::Cancel => { - return Ok(if app_server_target.supports_command_center() { - SessionStartOutcome::CommandCenter - } else { - SessionStartOutcome::Exit - }); - } + UnarchiveChoice::Cancel => return Ok(SessionStartOutcome::CommandCenter), UnarchiveChoice::Quit => return Ok(SessionStartOutcome::Exit), UnarchiveChoice::Unarchive => {} } diff --git a/codex-rs/tui/src/session_start_tests.rs b/codex-rs/tui/src/session_start_tests.rs index 74b6864d47..03e5ad9114 100644 --- a/codex-rs/tui/src/session_start_tests.rs +++ b/codex-rs/tui/src/session_start_tests.rs @@ -25,7 +25,6 @@ async fn start_session( config, local_settings: &local_settings, }, - &crate::AppServerTarget::Embedded, target, action, initial_result, @@ -74,57 +73,32 @@ async fn archived_session_requires_confirmation_before_resume_or_fork() -> Resul history_mode: None, }; let prompts = Cell::new(0); - let local_settings = LocalSettings::from(&config); - let endpoint = crate::resolve_remote_addr("ws://127.0.0.1:4500")?; - for server_target in [ - crate::AppServerTarget::LocalDaemon { - allow_embedded_fallback: true, - endpoint: endpoint.clone(), - }, - crate::AppServerTarget::Remote { endpoint }, - crate::AppServerTarget::Embedded, - ] { - for choice in [UnarchiveChoice::Cancel, UnarchiveChoice::Quit] { - let initial = action - .start(&mut app_server, &config, &local_settings, &target) - .await; - let outcome = complete_session_start( - &mut app_server, - SessionStartConfig { - config: &config, - local_settings: &local_settings, - }, - &server_target, - &target, - action, - initial, - async || { - prompts.set(prompts.get() + 1); - Ok(choice) - }, - ) - .await?; - assert_eq!( - matches!(outcome, SessionStartOutcome::CommandCenter), - choice == UnarchiveChoice::Cancel - && !matches!(server_target, crate::AppServerTarget::Embedded), - ); - assert_eq!( - (archived_path.exists(), active_path.exists()), - (true, false) - ); - assert!( - app_server - .thread_loaded_list(Default::default()) - .await? - .data - .is_empty() - ); - } + for choice in [UnarchiveChoice::Cancel, UnarchiveChoice::Quit] { + let outcome = start_session(&mut app_server, &config, &target, action, async || { + prompts.set(prompts.get() + 1); + Ok(choice) + }) + .await?; + assert!(matches!( + (choice, outcome), + (UnarchiveChoice::Cancel, SessionStartOutcome::CommandCenter) + | (UnarchiveChoice::Quit, SessionStartOutcome::Exit) + )); + assert_eq!( + (archived_path.exists(), active_path.exists()), + (true, false) + ); + assert!( + app_server + .thread_loaded_list(Default::default()) + .await? + .data + .is_empty() + ); } - assert_eq!(prompts.get(), 6); + assert_eq!(prompts.get(), 2); let SessionStartOutcome::Started(started) = start_session(&mut app_server, &config, &target, action, async || { prompts.set(prompts.get() + 1); @@ -136,7 +110,7 @@ async fn archived_session_requires_confirmation_before_resume_or_fork() -> Resul }; assert_eq!( (prompts.get(), archived_path.exists(), active_path.exists()), - (7, false, true) + (3, false, true) ); match action { SessionStartAction::Resume(..) => { diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_daemon_start_failed.snap b/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_daemon_start_failed.snap deleted file mode 100644 index 03fef87ad4..0000000000 --- a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_daemon_start_failed.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: tui/src/app/agents_overview_tests.rs -assertion_line: 401 -expression: rendered ---- -■ Failed to start the background server: The host does not allow detached processes diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_daemon_started.snap b/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_daemon_started.snap deleted file mode 100644 index db60e61ebd..0000000000 --- a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_daemon_started.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: tui/src/app/agents_overview_tests.rs -assertion_line: 401 -expression: rendered ---- -• Background server started. Run `codex agents` in another terminal; this session remains unchanged. diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_embedded.snap b/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_embedded.snap deleted file mode 100644 index 223ffd9eac..0000000000 --- a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_embedded.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: tui/src/app/agents_overview_tests.rs -expression: "render_bottom_popup(&app.chat_widget, 96)" ---- - Shared agents unavailable - This session isn’t connected to a shared background server. - - -› 1. Start background server Open `codex agents` in another terminal afterward - 2. Return to this session - - Starting a background server will not interrupt or move this session. - enter select · esc back diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap index 4918c7bf55..3b2b2eab4b 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap @@ -2,7 +2,7 @@ source: tui/src/keymap_setup.rs expression: snapshot --- -Open Agents | unbound | Global open_agents Open Agents Open the shared agent-session overview. unbound Default +Open Agents | unbound | Global open_agents Open Agents Open the agent command center. unbound Default Open Transcript | ctrl-t | Global open_transcript Open Transcript Open the transcript overlay. ctrl-t Default Find Transcript | f3 | Global find_transcript Find Transcript Find text in the full transcript. f3 Default Focus Activity | f4 | Global focus_activity Focus Activity Focus transcript activity groups to expand or collapse their details. f4 Default diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap index cc5ba997ed..f68a02d102 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap @@ -14,7 +14,7 @@ tab: Navigation (21 selectable) tab: Agents (11 selectable) tab: Approval (8 selectable) tab: Debug (1 selectable) -Open Agents | unbound | Global open_agents Open Agents Open the shared agent-session overview. unbound Default +Open Agents | unbound | Global open_agents Open Agents Open the agent command center. unbound Default Open Transcript | ctrl-t | Global open_transcript Open Transcript Open the transcript overlay. ctrl-t Default Find Transcript | f3 | Global find_transcript Find Transcript Find text in the full transcript. f3 Default Focus Activity | f4 | Global focus_activity Focus Activity Focus transcript activity groups to expand or collapse their details. f4 Default diff --git a/codex-rs/tui/src/startup_orchestration.rs b/codex-rs/tui/src/startup_orchestration.rs index 08c9b64bae..fded18b135 100644 --- a/codex-rs/tui/src/startup_orchestration.rs +++ b/codex-rs/tui/src/startup_orchestration.rs @@ -26,7 +26,6 @@ pub(super) async fn run_main_inner( #[cfg(windows)] let elevated_warning = if explicit_remote_endpoint.is_none() && !cli.no_daemon - && !cli.agents_overview && codex_app_server_daemon::is_elevated().map_err(std::io::Error::other)? { cli.no_daemon = true; @@ -568,9 +567,9 @@ pub(super) async fn run_main_inner( } else { None }; - let auto_start_daemon = config.features.enabled(Feature::DaemonAutoStart) + let auto_start_daemon = cfg!(any(unix, windows)) + && (cli.agents_overview || config.features.enabled(Feature::DaemonAutoStart)) && implicit_connection - && !cli.agents_overview && !cli.no_daemon && !app_server_target.uses_remote_workspace(); if auto_start_daemon @@ -608,7 +607,7 @@ pub(super) async fn run_main_inner( { daemon_exclusion = Some(daemon_startup::WSL_DRVFS_EXCLUSION); } - let mut managed_daemon = cli.managed_daemon; + let mut managed_daemon = false; if auto_start_daemon && daemon_exclusion.is_none() { let output = startup_draft .run_until(async { @@ -644,19 +643,14 @@ pub(super) async fn run_main_inner( daemon_exclusion = Some("this Windows launcher"); } } - // The overview must inspect the shared server's agents regardless of local settings. - let compatibility_warning = if cli.agents_overview { - None - } else { - daemon_recovery::check( - &mut startup_draft, - &app_server_target, - &config, - &cli_kv_overrides, - managed_daemon, - ) - .await? - }; + let compatibility_warning = daemon_recovery::check( + &mut startup_draft, + &app_server_target, + &config, + &cli_kv_overrides, + managed_daemon, + ) + .await?; if compatibility_warning.is_some() { app_server_target = AppServerTarget::Embedded; daemon_exclusion = Some("daemon feature settings");