diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 5cfb520dbed7..3164f16acb85 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -3,6 +3,7 @@ //! This module owns the `App` struct, shared imports, and the high-level run loop that coordinates //! the focused app submodules. +use crate::AppServerTarget; use crate::app_backtrack::BacktrackState; use crate::app_command::AppCommand; use crate::app_event::AppEvent; @@ -82,7 +83,6 @@ use crate::workspace_command::AppServerWorkspaceCommandRunner; use crate::workspace_command::WorkspaceCommandRunner; use codex_ansi_escape::ansi_escape_line; use codex_app_server_client::AppServerRequestHandle; -use codex_app_server_client::RemoteAppServerEndpoint; use codex_app_server_client::TypedRequestError; use codex_app_server_protocol::AddCreditsNudgeCreditType; use codex_app_server_protocol::AskForApproval; @@ -516,7 +516,7 @@ pub(crate) struct App { pub(crate) feedback: codex_feedback::CodexFeedback, feedback_audience: FeedbackAudience, environment_manager: Arc, - remote_app_server_endpoint: Option, + app_server_target: AppServerTarget, /// Set when the user confirms an update; propagated on exit. pub(crate) pending_update_action: Option, @@ -654,7 +654,7 @@ impl App { is_first_run: bool, entered_trust_nux: bool, should_prompt_windows_sandbox_nux_at_startup: bool, - remote_app_server_endpoint: Option, + app_server_target: AppServerTarget, state_db: Option, environment_manager: Arc, startup_hooks_browser: Option, @@ -941,7 +941,7 @@ See the Codex keymap documentation for supported actions and examples." feedback: feedback.clone(), feedback_audience, environment_manager, - remote_app_server_endpoint, + app_server_target, pending_update_action: None, pending_shutdown_exit_thread_id: None, windows_sandbox: WindowsSandboxState::default(), diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 987c9490d7b6..68bd759e2f63 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -57,10 +57,7 @@ impl App { AppEvent::OpenResumePicker => { let picker_app_server = match crate::start_app_server_for_picker( &self.config, - &match self.remote_app_server_endpoint.clone() { - Some(endpoint) => crate::AppServerTarget::Remote { endpoint }, - None => crate::AppServerTarget::Embedded, - }, + &self.app_server_target, self.state_db.clone(), self.environment_manager.clone(), ) @@ -1680,7 +1677,7 @@ impl App { { Ok(()) => { self.chat_widget.update_skill_enabled(path, enabled); - if !app_server.is_remote() + if !app_server.uses_remote_workspace() && let Err(err) = self.refresh_in_memory_config_from_disk().await { tracing::warn!( @@ -1724,7 +1721,7 @@ impl App { { Ok(_) => { self.chat_widget.update_connector_enabled(&id, enabled); - if !app_server.is_remote() + if !app_server.uses_remote_workspace() && let Err(err) = self.refresh_in_memory_config_from_disk().await { tracing::warn!(error = %err, "failed to refresh config after app toggle"); diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index 1a57b596b0ff..6ea53d8b3cb0 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -633,7 +633,7 @@ impl App { } let current_cwd = self.config.cwd.to_path_buf(); - let resume_cwd = if self.remote_app_server_endpoint.is_some() { + let resume_cwd = if self.app_server_target.uses_remote_workspace() { current_cwd.clone() } else { match crate::session_resume::resolve_cwd_for_resume_or_fork( diff --git a/codex-rs/tui/src/app/test_support.rs b/codex-rs/tui/src/app/test_support.rs index a22107c22640..6b6c4242102d 100644 --- a/codex-rs/tui/src/app/test_support.rs +++ b/codex-rs/tui/src/app/test_support.rs @@ -45,7 +45,7 @@ pub(super) async fn make_test_app() -> App { feedback: codex_feedback::CodexFeedback::new(), feedback_audience: FeedbackAudience::External, environment_manager: Arc::new(EnvironmentManager::default_for_tests()), - remote_app_server_endpoint: None, + app_server_target: crate::AppServerTarget::Embedded, pending_update_action: None, pending_shutdown_exit_thread_id: None, windows_sandbox: WindowsSandboxState::default(), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 547f70890c08..bddf5e544c3d 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -3867,7 +3867,7 @@ async fn make_test_app() -> App { feedback: codex_feedback::CodexFeedback::new(), feedback_audience: FeedbackAudience::External, environment_manager: Arc::new(EnvironmentManager::default_for_tests()), - remote_app_server_endpoint: None, + app_server_target: crate::AppServerTarget::Embedded, pending_update_action: None, pending_shutdown_exit_thread_id: None, windows_sandbox: WindowsSandboxState::default(), @@ -3930,7 +3930,7 @@ async fn make_test_app_with_channels() -> ( feedback: codex_feedback::CodexFeedback::new(), feedback_audience: FeedbackAudience::External, environment_manager: Arc::new(EnvironmentManager::default_for_tests()), - remote_app_server_endpoint: None, + app_server_target: crate::AppServerTarget::Embedded, pending_update_action: None, pending_shutdown_exit_thread_id: None, windows_sandbox: WindowsSandboxState::default(), diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 5ea7bccf9719..1bcb4a559736 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -149,10 +149,11 @@ pub(crate) struct AppServerSession { client: AppServerClient, next_request_id: i64, remote_cwd_override: Option, + thread_params_mode: ThreadParamsMode, } -#[derive(Clone, Copy)] -enum ThreadParamsMode { +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ThreadParamsMode { Embedded, Remote, } @@ -182,11 +183,12 @@ pub(crate) enum TurnPermissionsOverride { } impl AppServerSession { - pub(crate) fn new(client: AppServerClient) -> Self { + pub(crate) fn new(client: AppServerClient, thread_params_mode: ThreadParamsMode) -> Self { Self { client, next_request_id: 1, remote_cwd_override: None, + thread_params_mode, } } @@ -199,8 +201,8 @@ impl AppServerSession { self.remote_cwd_override.as_deref() } - pub(crate) fn is_remote(&self) -> bool { - matches!(self.client, AppServerClient::Remote(_)) + pub(crate) fn uses_remote_workspace(&self) -> bool { + matches!(self.thread_params_mode, ThreadParamsMode::Remote) } pub(crate) async fn bootstrap(&mut self, config: &Config) -> Result { @@ -426,10 +428,7 @@ impl AppServerSession { } fn thread_params_mode(&self) -> ThreadParamsMode { - match &self.client { - AppServerClient::InProcess(_) => ThreadParamsMode::Embedded, - AppServerClient::Remote(_) => ThreadParamsMode::Remote, - } + self.thread_params_mode } async fn fork_parent_title_from_app_server( diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 4281ad29237a..40cd121c7135 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -20,6 +20,7 @@ use app::App; pub use app::AppExitInfo; pub use app::ExitReason; use app_server_session::AppServerSession; +use app_server_session::ThreadParamsMode; use codex_app_server_client::AppServerClient; use codex_app_server_client::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY; use codex_app_server_client::InProcessAppServerClient; @@ -312,9 +313,24 @@ async fn start_embedded_app_server( #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum AppServerTarget { Embedded, + LocalDaemon { endpoint: RemoteAppServerEndpoint }, Remote { endpoint: RemoteAppServerEndpoint }, } +impl AppServerTarget { + pub(crate) fn uses_remote_workspace(&self) -> bool { + matches!(self, Self::Remote { .. }) + } + + fn thread_params_mode(&self) -> ThreadParamsMode { + if self.uses_remote_workspace() { + ThreadParamsMode::Remote + } else { + ThreadParamsMode::Embedded + } + } +} + async fn init_state_db_for_app_server_target( config: &Config, app_server_target: &AppServerTarget, @@ -326,7 +342,9 @@ async fn init_state_db_for_app_server_target( err.to_string(), )) }), - AppServerTarget::Remote { .. } => Ok(state_db::get_state_db(config).await), + AppServerTarget::LocalDaemon { .. } | AppServerTarget::Remote { .. } => { + Ok(state_db::get_state_db(config).await) + } } } @@ -496,7 +514,9 @@ async fn start_app_server( ) .await .map(AppServerClient::InProcess), - AppServerTarget::Remote { endpoint } => connect_remote_app_server(endpoint.clone()).await, + AppServerTarget::LocalDaemon { endpoint } | AppServerTarget::Remote { endpoint } => { + connect_remote_app_server(endpoint.clone()).await + } } } @@ -520,7 +540,10 @@ pub(crate) async fn start_app_server_for_picker( environment_manager, ) .await?; - Ok(AppServerSession::new(app_server)) + Ok(AppServerSession::new( + app_server, + target.thread_params_mode(), + )) } #[cfg(test)] @@ -695,7 +718,7 @@ async fn lookup_latest_session_target_with_app_server( ) -> color_eyre::Result> { let response = app_server .thread_list(latest_session_lookup_params( - app_server.is_remote(), + app_server.uses_remote_workspace(), config, cwd_filter, include_non_interactive, @@ -708,7 +731,7 @@ async fn lookup_latest_session_target_with_app_server( } fn latest_session_lookup_params( - is_remote: bool, + uses_remote_workspace: bool, config: &Config, cwd_filter: Option<&Path>, include_non_interactive: bool, @@ -718,7 +741,7 @@ fn latest_session_lookup_params( limit: Some(1), sort_key: Some(AppServerThreadSortKey::UpdatedAt), sort_direction: None, - model_providers: if is_remote { + model_providers: if uses_remote_workspace { None } else { Some(vec![config.model_provider_id.clone()]) @@ -737,7 +760,7 @@ fn config_cwd_for_app_server_target( app_server_target: &AppServerTarget, environment_manager: &EnvironmentManager, ) -> std::io::Result> { - if matches!(app_server_target, AppServerTarget::Remote { .. }) + if app_server_target.uses_remote_workspace() || environment_manager .default_environment() .is_some_and(|environment| environment.is_remote()) @@ -758,12 +781,11 @@ fn should_load_configured_environments( loader_overrides: &LoaderOverrides, app_server_target: &AppServerTarget, ) -> bool { - !loader_overrides.ignore_user_config - && !matches!(app_server_target, AppServerTarget::Remote { .. }) + !loader_overrides.ignore_user_config && !app_server_target.uses_remote_workspace() } fn latest_session_cwd_filter<'a>( - remote_mode: bool, + uses_remote_workspace: bool, remote_cwd_override: Option<&'a Path>, config: &'a Config, show_all: bool, @@ -772,13 +794,62 @@ fn latest_session_cwd_filter<'a>( return None; } - if remote_mode { + if uses_remote_workspace { remote_cwd_override } else { Some(config.cwd.as_path()) } } +fn app_server_target_for_launch( + explicit_remote_endpoint: Option, + default_daemon_socket: Option, + can_reuse_implicit_local_daemon: bool, +) -> AppServerTarget { + match explicit_remote_endpoint { + Some(endpoint) => AppServerTarget::Remote { endpoint }, + None if can_reuse_implicit_local_daemon => { + default_daemon_socket.map_or(AppServerTarget::Embedded, |socket_path| { + AppServerTarget::LocalDaemon { + endpoint: RemoteAppServerEndpoint::UnixSocket { socket_path }, + } + }) + } + None => AppServerTarget::Embedded, + } +} + +fn loader_overrides_are_default(loader_overrides: &LoaderOverrides) -> bool { + let loader_overrides_are_default = loader_overrides.user_config_path.is_none() + && loader_overrides.user_config_profile.is_none() + && loader_overrides.managed_config_path.is_none() + && loader_overrides.system_config_path.is_none() + && loader_overrides.system_requirements_path.is_none() + && !loader_overrides.ignore_managed_requirements + && !loader_overrides.ignore_user_config + && !loader_overrides.ignore_user_and_project_exec_policy_rules + && loader_overrides + .macos_managed_config_requirements_base64 + .is_none(); + #[cfg(target_os = "macos")] + let loader_overrides_are_default = + loader_overrides_are_default && loader_overrides.managed_preferences_base64.is_none(); + loader_overrides_are_default +} + +fn can_reuse_implicit_local_daemon( + cli_kv_overrides: &[(String, toml::Value)], + loader_overrides: &LoaderOverrides, + strict_config: bool, + has_non_replayable_launch_overrides: bool, +) -> bool { + // A reused daemon cannot adopt this invocation's full launch config state. + cli_kv_overrides.is_empty() + && loader_overrides_are_default(loader_overrides) + && !strict_config + && !has_non_replayable_launch_overrides +} + pub async fn run_main( mut cli: Cli, arg0_paths: Arg0DispatchPaths, @@ -830,21 +901,32 @@ pub async fn run_main( } }; - let remote_endpoint = match explicit_remote_endpoint { - Some(endpoint) => Some(endpoint), - None => maybe_probe_default_daemon_socket(&codex_home) - .await - .map(|socket_path| RemoteAppServerEndpoint::UnixSocket { socket_path }), + let mut launch_loader_overrides = loader_overrides.clone(); + if let Some(profile_v2) = cli.config_profile_v2.as_ref() { + let user_config_path = resolve_profile_v2_config_path(&codex_home, profile_v2); + launch_loader_overrides.user_config_path = Some(user_config_path); + launch_loader_overrides.user_config_profile = Some(profile_v2.clone()); + } + let reuse_implicit_local_daemon = can_reuse_implicit_local_daemon( + &cli_kv_overrides, + &launch_loader_overrides, + strict_config, + cli.bypass_hook_trust, + ); + let default_daemon = if explicit_remote_endpoint.is_none() && reuse_implicit_local_daemon { + maybe_probe_default_daemon_socket(&codex_home).await + } else { + None }; - let app_server_target = remote_endpoint - .clone() - .map_or(AppServerTarget::Embedded, |endpoint| { - AppServerTarget::Remote { endpoint } - }); + let app_server_target = app_server_target_for_launch( + explicit_remote_endpoint, + default_daemon, + reuse_implicit_local_daemon, + ); let remote_cwd_override = cli .cwd .clone() - .filter(|_| matches!(app_server_target, AppServerTarget::Remote { .. })); + .filter(|_| app_server_target.uses_remote_workspace()); let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths( arg0_paths.codex_self_exe.clone(), @@ -952,7 +1034,7 @@ pub async fn run_main( model, approval_policy, sandbox_mode, - cwd: if matches!(app_server_target, AppServerTarget::Remote { .. }) { + cwd: if app_server_target.uses_remote_workspace() { None } else { cwd @@ -1072,7 +1154,7 @@ pub async fn run_main( } } - if matches!(app_server_target, AppServerTarget::Embedded) { + if !app_server_target.uses_remote_workspace() { #[allow(clippy::print_stderr)] if let Err(err) = enforce_login_restrictions(&AuthConfig { codex_home: config.codex_home.to_path_buf(), @@ -1180,7 +1262,6 @@ pub async fn run_main( feedback, log_db, state_db, - remote_endpoint, environment_manager, ) .await @@ -1202,10 +1283,9 @@ async fn run_ratatui_app( feedback: codex_feedback::CodexFeedback, log_db: Option, state_db: Option, - remote_endpoint: Option, environment_manager: Arc, ) -> color_eyre::Result { - let remote_mode = matches!(&app_server_target, AppServerTarget::Remote { .. }); + let uses_remote_workspace = app_server_target.uses_remote_workspace(); color_eyre::install()?; tooltips::announcement::prewarm(); @@ -1268,7 +1348,7 @@ async fn run_ratatui_app( ) .await { - Ok(app_server) => AppServerSession::new(app_server), + Ok(app_server) => AppServerSession::new(app_server, app_server_target.thread_params_mode()), Err(err) => { terminal_restore_guard.restore_silently(); session_log::log_session_end(); @@ -1278,7 +1358,8 @@ async fn run_ratatui_app( .with_remote_cwd_override(remote_cwd_override.clone()); let mut app_server = Some(app_server_session); - let should_show_trust_screen_flag = !remote_mode && should_show_trust_screen(&initial_config); + let should_show_trust_screen_flag = + !uses_remote_workspace && should_show_trust_screen(&initial_config); let mut trust_decision_was_made = false; let login_status = if initial_config.model_provider.requires_openai_auth { let Some(app_server) = app_server.as_mut() else { @@ -1328,7 +1409,7 @@ async fn run_ratatui_app( // If this onboarding run included the login step, always refresh cloud requirements and // rebuild config. This avoids missing newly available cloud requirements due to login // status detection edge cases. - if show_login_screen && !remote_mode { + if show_login_screen && !uses_remote_workspace { cloud_requirements = cloud_requirements_loader_for_storage( initial_config.codex_home.to_path_buf(), /*enable_codex_api_key_env*/ false, @@ -1341,7 +1422,7 @@ async fn run_ratatui_app( // If the user made an explicit trust decision, or we showed the login flow, reload config // so current process state reflects persisted trust/auth changes. if onboarding_result.directory_trust_decision.is_some() - || (show_login_screen && !remote_mode) + || (show_login_screen && !uses_remote_workspace) { load_config_or_exit( cli_kv_overrides.clone(), @@ -1389,7 +1470,7 @@ async fn run_ratatui_app( } } else if cli.fork_last { let filter_cwd = latest_session_cwd_filter( - remote_mode, + uses_remote_workspace, remote_cwd_override.as_deref(), &config, cli.fork_show_all, @@ -1446,7 +1527,7 @@ async fn run_ratatui_app( } } else if cli.resume_last { let filter_cwd = latest_session_cwd_filter( - remote_mode, + uses_remote_workspace, remote_cwd_override.as_deref(), &config, cli.resume_show_all, @@ -1496,7 +1577,7 @@ async fn run_ratatui_app( }; let current_cwd = config.cwd.clone(); - let allow_prompt = !remote_mode && cli.cwd.is_none(); + let allow_prompt = !uses_remote_workspace && cli.cwd.is_none(); let action_and_target_session_if_resume_or_fork = match &session_selection { resume_picker::SessionSelection::Resume(target_session) => { Some((CwdPromptAction::Resume, target_session)) @@ -1508,7 +1589,7 @@ async fn run_ratatui_app( }; let fallback_cwd = match action_and_target_session_if_resume_or_fork { Some((action, target_session)) => { - if remote_mode { + if uses_remote_workspace { Some(current_cwd.to_path_buf()) } else { match resolve_cwd_for_resume_or_fork( @@ -1614,8 +1695,10 @@ async fn run_ratatui_app( ) .await { - Ok(app_server) => AppServerSession::new(app_server) - .with_remote_cwd_override(remote_cwd_override.clone()), + Ok(app_server) => { + AppServerSession::new(app_server, app_server_target.thread_params_mode()) + .with_remote_cwd_override(remote_cwd_override.clone()) + } Err(err) => { terminal_restore_guard.restore_silently(); session_log::log_session_end(); @@ -1645,7 +1728,7 @@ async fn run_ratatui_app( should_show_trust_screen, // Proxy to: is it a first run in this directory? should_show_trust_screen_flag, // Preserve the startup-time trust NUX signal before onboarding should_prompt_windows_sandbox_nux_at_startup, - remote_endpoint, + app_server_target, state_db, environment_manager, startup_hooks_browser, @@ -1989,6 +2072,117 @@ mod tests { Ok(()) } + #[test] + fn app_server_target_for_launch_uses_local_daemon_for_default_socket() -> color_eyre::Result<()> + { + let socket_path = AbsolutePathBuf::relative_to_current_dir("codex.sock")?; + let target = app_server_target_for_launch( + /*explicit_remote_endpoint*/ None, + Some(socket_path.clone()), + /*can_reuse_implicit_local_daemon*/ true, + ); + + assert_eq!( + target, + AppServerTarget::LocalDaemon { + endpoint: RemoteAppServerEndpoint::UnixSocket { socket_path }, + } + ); + assert!(!target.uses_remote_workspace()); + assert_eq!(target.thread_params_mode(), ThreadParamsMode::Embedded); + Ok(()) + } + + #[test] + fn app_server_target_for_launch_prefers_explicit_remote_endpoint() -> color_eyre::Result<()> { + let explicit_endpoint = RemoteAppServerEndpoint::UnixSocket { + socket_path: AbsolutePathBuf::relative_to_current_dir("explicit.sock")?, + }; + let target = app_server_target_for_launch( + Some(explicit_endpoint.clone()), + Some(AbsolutePathBuf::relative_to_current_dir("default.sock")?), + /*can_reuse_implicit_local_daemon*/ false, + ); + + assert_eq!( + target, + AppServerTarget::Remote { + endpoint: explicit_endpoint, + } + ); + assert!(target.uses_remote_workspace()); + assert_eq!(target.thread_params_mode(), ThreadParamsMode::Remote); + Ok(()) + } + + #[test] + fn app_server_target_for_launch_skips_local_daemon_when_launch_config_is_not_replayable() + -> color_eyre::Result<()> { + let socket_path = AbsolutePathBuf::relative_to_current_dir("codex.sock")?; + let target = app_server_target_for_launch( + /*explicit_remote_endpoint*/ None, + Some(socket_path), + /*can_reuse_implicit_local_daemon*/ false, + ); + + assert_eq!(target, AppServerTarget::Embedded); + Ok(()) + } + + #[test] + fn can_reuse_implicit_local_daemon_requires_default_launch_config() -> color_eyre::Result<()> { + let mut loader_overrides = LoaderOverrides::default(); + let cli_kv_overrides = vec![("web_search".to_string(), toml::Value::String("live".into()))]; + + assert!(can_reuse_implicit_local_daemon( + &[], + &LoaderOverrides::default(), + /*strict_config*/ false, + /*has_non_replayable_launch_overrides*/ false, + )); + assert!(!can_reuse_implicit_local_daemon( + &cli_kv_overrides, + &LoaderOverrides::default(), + /*strict_config*/ false, + /*has_non_replayable_launch_overrides*/ false, + )); + loader_overrides.ignore_user_config = true; + assert!(!can_reuse_implicit_local_daemon( + &[], + &loader_overrides, + /*strict_config*/ false, + /*has_non_replayable_launch_overrides*/ false, + )); + assert!(!can_reuse_implicit_local_daemon( + &[], + &LoaderOverrides::default(), + /*strict_config*/ true, + /*has_non_replayable_launch_overrides*/ false, + )); + assert!(!can_reuse_implicit_local_daemon( + &[], + &LoaderOverrides::default(), + /*strict_config*/ false, + /*has_non_replayable_launch_overrides*/ true, + )); + Ok(()) + } + + #[test] + fn should_load_configured_environments_for_local_daemon() -> color_eyre::Result<()> { + let target = AppServerTarget::LocalDaemon { + endpoint: RemoteAppServerEndpoint::UnixSocket { + socket_path: AbsolutePathBuf::relative_to_current_dir("codex.sock")?, + }, + }; + + assert!(should_load_configured_environments( + &LoaderOverrides::default(), + &target, + )); + Ok(()) + } + #[tokio::test] async fn latest_session_lookup_params_keep_local_filters_for_embedded_sessions() -> std::io::Result<()> { @@ -1997,7 +2191,34 @@ mod tests { let cwd = temp_dir.path().join("project"); let params = latest_session_lookup_params( - /*is_remote*/ false, + /*uses_remote_workspace*/ false, + &config, + Some(cwd.as_path()), + /*include_non_interactive*/ false, + ); + + assert_eq!(params.model_providers, Some(vec![config.model_provider_id])); + assert_eq!( + params.cwd, + Some(ThreadListCwdFilter::One(cwd.to_string_lossy().to_string())) + ); + Ok(()) + } + + #[tokio::test] + async fn latest_session_lookup_params_keep_local_filters_for_local_daemon_sessions() + -> color_eyre::Result<()> { + let temp_dir = TempDir::new()?; + let config = build_config(&temp_dir).await?; + let cwd = temp_dir.path().join("project"); + let target = AppServerTarget::LocalDaemon { + endpoint: RemoteAppServerEndpoint::UnixSocket { + socket_path: AbsolutePathBuf::relative_to_current_dir("codex.sock")?, + }, + }; + + let params = latest_session_lookup_params( + target.uses_remote_workspace(), &config, Some(cwd.as_path()), /*include_non_interactive*/ false, @@ -2018,7 +2239,7 @@ mod tests { let config = build_config(&temp_dir).await?; let params = latest_session_lookup_params( - /*is_remote*/ true, &config, /*cwd_filter*/ None, + /*uses_remote_workspace*/ true, &config, /*cwd_filter*/ None, /*include_non_interactive*/ false, ); @@ -2035,7 +2256,7 @@ mod tests { let cwd = Path::new("repo/on/server"); let params = latest_session_lookup_params( - /*is_remote*/ true, + /*uses_remote_workspace*/ true, &config, Some(cwd), /*include_non_interactive*/ false, @@ -2056,15 +2277,15 @@ mod tests { let remote_cwd = Path::new("repo/on/server"); let local_filter = latest_session_cwd_filter( - /*remote_mode*/ false, /*remote_cwd_override*/ None, &config, + /*uses_remote_workspace*/ false, /*remote_cwd_override*/ None, &config, /*show_all*/ false, ); let show_all_filter = latest_session_cwd_filter( - /*remote_mode*/ false, /*remote_cwd_override*/ None, &config, + /*uses_remote_workspace*/ false, /*remote_cwd_override*/ None, &config, /*show_all*/ true, ); let remote_filter = latest_session_cwd_filter( - /*remote_mode*/ true, + /*uses_remote_workspace*/ true, Some(remote_cwd), &config, /*show_all*/ false, @@ -2189,12 +2410,14 @@ mod tests { &other_cwd, )?; - let mut app_server = - AppServerSession::new(codex_app_server_client::AppServerClient::InProcess( + let mut app_server = AppServerSession::new( + codex_app_server_client::AppServerClient::InProcess( start_test_embedded_app_server(config.clone()).await?, - )); + ), + ThreadParamsMode::Embedded, + ); let filter_cwd = latest_session_cwd_filter( - /*remote_mode*/ false, /*remote_cwd_override*/ None, &config, + /*uses_remote_workspace*/ false, /*remote_cwd_override*/ None, &config, /*show_all*/ false, ); let scoped_target = lookup_latest_session_target_with_app_server( @@ -2206,7 +2429,7 @@ mod tests { .await? .expect("expected project-scoped fork --last target"); let show_all_filter_cwd = latest_session_cwd_filter( - /*remote_mode*/ false, /*remote_cwd_override*/ None, &config, + /*uses_remote_workspace*/ false, /*remote_cwd_override*/ None, &config, /*show_all*/ true, ); let show_all_target = lookup_latest_session_target_with_app_server( @@ -2265,6 +2488,29 @@ mod tests { Ok(()) } + #[tokio::test] + async fn config_cwd_for_app_server_target_canonicalizes_local_daemon_cli_cwd() + -> std::io::Result<()> { + let temp_dir = TempDir::new()?; + let target = AppServerTarget::LocalDaemon { + endpoint: RemoteAppServerEndpoint::UnixSocket { + socket_path: AbsolutePathBuf::relative_to_current_dir("codex.sock")?, + }, + }; + let environment_manager = EnvironmentManager::default_for_tests(); + + let config_cwd = + config_cwd_for_app_server_target(Some(temp_dir.path()), &target, &environment_manager)?; + + assert_eq!( + config_cwd, + Some(AbsolutePathBuf::from_absolute_path(dunce::canonicalize( + temp_dir.path() + )?)?) + ); + Ok(()) + } + #[tokio::test] async fn config_cwd_for_app_server_target_errors_for_missing_embedded_cli_cwd() -> std::io::Result<()> { @@ -2388,10 +2634,12 @@ mod tests { .await .map_err(std::io::Error::other)?; - let mut app_server = - AppServerSession::new(codex_app_server_client::AppServerClient::InProcess( + let mut app_server = AppServerSession::new( + codex_app_server_client::AppServerClient::InProcess( start_test_embedded_app_server(config).await?, - )); + ), + ThreadParamsMode::Embedded, + ); let target = lookup_session_target_by_name_with_app_server(&mut app_server, "saved-session") .await?; diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index c7be652f85b6..3dbc2c3da606 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -349,15 +349,15 @@ async fn run_resume_picker_with_launch_context( launch_context: SessionPickerLaunchContext, ) -> Result { let (bg_tx, bg_rx) = mpsc::unbounded_channel(); - let is_remote = app_server.is_remote(); + let uses_remote_workspace = app_server.uses_remote_workspace(); let cwd_filter = picker_cwd_filter( config.cwd.as_path(), /*show_all*/ false, - is_remote, + uses_remote_workspace, app_server.remote_cwd_override(), ); - let local_filter_cwd = local_picker_cwd_filter(&cwd_filter, is_remote); - let provider_filter = picker_provider_filter(config, is_remote); + let local_filter_cwd = local_picker_cwd_filter(&cwd_filter, uses_remote_workspace); + let provider_filter = picker_provider_filter(config, uses_remote_workspace); let runtime_keymap = picker_runtime_keymap(config)?; let options = SessionPickerRunOptions { show_all, @@ -395,15 +395,15 @@ pub async fn run_fork_picker_with_app_server( app_server: AppServerSession, ) -> Result { let (bg_tx, bg_rx) = mpsc::unbounded_channel(); - let is_remote = app_server.is_remote(); + let uses_remote_workspace = app_server.uses_remote_workspace(); let cwd_filter = picker_cwd_filter( config.cwd.as_path(), /*show_all*/ false, - is_remote, + uses_remote_workspace, app_server.remote_cwd_override(), ); - let local_filter_cwd = local_picker_cwd_filter(&cwd_filter, is_remote); - let provider_filter = picker_provider_filter(config, is_remote); + let local_filter_cwd = local_picker_cwd_filter(&cwd_filter, uses_remote_workspace); + let provider_filter = picker_provider_filter(config, uses_remote_workspace); let runtime_keymap = picker_runtime_keymap(config)?; let options = SessionPickerRunOptions { show_all, @@ -513,12 +513,19 @@ fn raw_reasoning_visibility(config: &Config) -> RawReasoningVisibility { } } -fn local_picker_cwd_filter(cwd_filter: &Option, is_remote: bool) -> Option { - if is_remote { None } else { cwd_filter.clone() } +fn local_picker_cwd_filter( + cwd_filter: &Option, + uses_remote_workspace: bool, +) -> Option { + if uses_remote_workspace { + None + } else { + cwd_filter.clone() + } } -fn picker_provider_filter(config: &Config, is_remote: bool) -> ProviderFilter { - if is_remote { +fn picker_provider_filter(config: &Config, uses_remote_workspace: bool) -> ProviderFilter { + if uses_remote_workspace { ProviderFilter::Any } else { ProviderFilter::MatchDefault(config.model_provider_id.to_string()) @@ -533,12 +540,12 @@ fn picker_runtime_keymap(config: &Config) -> Result { fn picker_cwd_filter( config_cwd: &Path, show_all: bool, - is_remote: bool, + uses_remote_workspace: bool, remote_cwd_override: Option<&Path>, ) -> Option { if show_all { None - } else if is_remote { + } else if uses_remote_workspace { remote_cwd_override.map(Path::to_path_buf) } else { Some(config_cwd.to_path_buf()) @@ -3309,7 +3316,7 @@ mod tests { let cwd_filter = picker_cwd_filter( Path::new("/tmp/project"), /*show_all*/ false, - /*is_remote*/ false, + /*uses_remote_workspace*/ false, /*remote_cwd_override*/ None, ); let params = thread_list_params( @@ -3588,7 +3595,8 @@ mod tests { remote_cwd.clone(), SessionPickerAction::Resume, ); - state.local_filter_cwd = local_picker_cwd_filter(&remote_cwd, /*is_remote*/ true); + state.local_filter_cwd = + local_picker_cwd_filter(&remote_cwd, /*uses_remote_workspace*/ true); state.start_initial_load();