From 38cdc4d65561e35848d252276f015fbd909efb9b Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Fri, 13 Nov 2026 22:52:28 +0000 Subject: [PATCH] Show the primary executor's workspace in TUI status surfaces (#61405) ## Why The primary executor's working directory can differ from the app-server's. Displaying the app-server directory can misidentify the workspace, and abbreviating a remote path using the TUI's home directory can be misleading. ## What changed - Carry the primary environment's working directory through thread initialization, restoration, and settings updates. - Use it for the status line and terminal title's current-directory item, and expose it as `thread.workspace.cwd` in the statusline state. - Preserve the known environment directory when settings omit environments; fall back to the app-server directory when the environment list is empty. - Apply home-directory abbreviation only to server-local workspaces on local connections. ## Testing Add regression coverage for remote executor paths, omitted and empty environment settings, local-only directory abbreviation, and workspace state for inactive threads. Update the resumed-thread snapshot to display the executor directory. GitOrigin-RevId: 9f3bcad186e07f942ea00d1f4233cf09f022571f --- codex-rs/tui/src/app/config_persistence.rs | 1 + .../tui/src/app/owned_transcript_tests.rs | 1 + codex-rs/tui/src/app/tests.rs | 9 ++- codex-rs/tui/src/app/thread_events.rs | 1 + codex-rs/tui/src/app/thread_routing.rs | 7 ++ codex-rs/tui/src/app/thread_session_state.rs | 7 ++ codex-rs/tui/src/app/thread_settings.rs | 4 ++ codex-rs/tui/src/app_server_session.rs | 23 ++++--- .../src/app_server_session/rollout_history.rs | 2 +- codex-rs/tui/src/chatwidget.rs | 1 + codex-rs/tui/src/chatwidget/constructor.rs | 1 + codex-rs/tui/src/chatwidget/session_flow.rs | 2 + codex-rs/tui/src/chatwidget/settings.rs | 4 ++ .../tui/src/chatwidget/status_surfaces.rs | 34 +++++++-- .../tui/src/chatwidget/statusline_state.rs | 11 +++ .../tui/src/chatwidget/tests/app_server.rs | 69 +++++++++++++++++++ .../chatwidget/tests/composer_submission.rs | 9 +++ .../tui/src/chatwidget/tests/exec_flow.rs | 1 + .../src/chatwidget/tests/history_replay.rs | 11 +++ .../tui/src/chatwidget/tests/permissions.rs | 3 + .../tui/src/chatwidget/tests/plan_mode.rs | 2 + .../src/chatwidget/tests/status_and_layout.rs | 1 + .../tests/statusline_state_tests.rs | 2 + codex-rs/tui/src/history_cell/tests.rs | 1 + codex-rs/tui/src/session_state.rs | 19 +++++ ...ver_session__tests__resumed_plan_mode.snap | 2 +- 26 files changed, 210 insertions(+), 18 deletions(-) diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 761dc99813..e0c78f9ea1 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -1944,6 +1944,7 @@ enabled = false .handle_thread_session(crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), diff --git a/codex-rs/tui/src/app/owned_transcript_tests.rs b/codex-rs/tui/src/app/owned_transcript_tests.rs index 378193a10f..cd4c1fb4ba 100644 --- a/codex-rs/tui/src/app/owned_transcript_tests.rs +++ b/codex-rs/tui/src/app/owned_transcript_tests.rs @@ -37,6 +37,7 @@ pub(in crate::app) fn attach_thread(app: &mut App, thread_id: ThreadId) { app.chat_widget.handle_thread_session(ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 65cf990143..aadfaf95fc 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -7518,7 +7518,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re originator: None, environments: Some(vec![codex_app_server_protocol::ThreadEnvironment { environment_id: "remote".to_string(), - cwd: test_path_buf("/tmp/agent").abs().into(), + cwd: codex_utils_path_uri::LegacyAppPathString::from_string("/executor/agent"), runtime_workspace_roots: Vec::new(), }]), id: agent_thread_id.to_string(), @@ -7596,6 +7596,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re app.chat_widget.handle_thread_session(session); let state = serde_json::to_value(app.chat_widget.statusline_state())?; let thread = &state["thread"]; + assert_eq!(thread["workspace"]["cwd"], "/executor/agent"); assert_eq!(thread["model"]["id"], "gpt-agent"); assert_eq!(thread["model"]["reasoning_level"], "low"); assert_eq!(thread["model"]["access_program"], "daybreak_blue"); @@ -8871,6 +8872,7 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String { let session = ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -9586,6 +9588,7 @@ pub(super) fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSe ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -10904,6 +10907,7 @@ async fn backtrack_selection_preserves_selected_prompt_and_requests_branch() { let session = ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -10979,6 +10983,7 @@ async fn backtrack_selection_preserves_selected_prompt_and_requests_branch() { .handle_thread_session(crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: base_id, @@ -12377,6 +12382,7 @@ async fn new_session_requests_shutdown_for_previous_conversation() { let event = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -13158,6 +13164,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() { .handle_thread_session(crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index 05cb71a115..b8f4e97ddc 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -746,6 +746,7 @@ mod tests { ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index cf373ae4ba..4b49130539 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -1432,6 +1432,13 @@ impl App { let mut session = self.primary_session_configured.clone()?; session.read_only_history = false; session.thread_settings_confirmed = false; + session.primary_environment_cwd = crate::session_state::primary_environment_cwd( + notification + .thread + .environments + .as_deref() + .unwrap_or_default(), + ); session.thread_id = thread_id; // The primary session supplies defaults. The new thread supplies its own fork parent. session.set_fork_parent(notification.thread.forked_from_id.as_deref()); diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index 89dff8b416..40bd9d9f9b 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -83,6 +83,7 @@ impl App { // `thread/read` does not include all thread settings, so do not carry // thread-scoped state from the currently active session. session.thread_settings_confirmed = false; + session.primary_environment_cwd = None; session.collaboration_mode = None; session.daybreak_enabled = false; } @@ -91,6 +92,7 @@ impl App { ThreadSessionState { read_only_history: true, thread_settings_confirmed: false, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -116,6 +118,10 @@ impl App { rollout_path: thread.path.clone(), } }; + if let Some(environments) = &thread.environments { + session.primary_environment_cwd = + crate::session_state::primary_environment_cwd(environments); + } session.windows_sandbox_host = crate::windows_sandbox::host_from_environments(thread.environments.as_deref()); session.thread_id = thread_id; @@ -180,6 +186,7 @@ mod tests { ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/app/thread_settings.rs b/codex-rs/tui/src/app/thread_settings.rs index 98b0547612..f33597b493 100644 --- a/codex-rs/tui/src/app/thread_settings.rs +++ b/codex-rs/tui/src/app/thread_settings.rs @@ -290,6 +290,10 @@ pub(super) fn apply_thread_settings_to_session( settings.cwd.as_path(), ); session.active_permission_profile = settings.active_permission_profile.clone().map(Into::into); + if let Some(environments) = &settings.environments { + session.primary_environment_cwd = + crate::session_state::primary_environment_cwd(environments); + } session.set_cwd_retargeting_implicit_runtime_workspace_root(settings.cwd.clone()); let mut collaboration_mode = settings.collaboration_mode.clone(); collaboration_mode diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 3a4e8c25e7..acc5b2d466 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -2973,7 +2973,7 @@ async fn thread_session_state_from_thread_start_response( ); let mut session = thread_session_state_from_thread_response( &response.thread.id, - crate::windows_sandbox::host_from_environments(response.thread.environments.as_deref()), + response.thread.environments.as_deref(), response.thread.forked_from_id.clone(), response.thread.name.clone(), response.thread.path.clone(), @@ -3011,7 +3011,7 @@ async fn thread_session_state_from_thread_resume_response( ); let mut session = thread_session_state_from_thread_response( &response.thread.id, - crate::windows_sandbox::host_from_environments(response.thread.environments.as_deref()), + response.thread.environments.as_deref(), response.thread.forked_from_id.clone(), response.thread.name.clone(), response.thread.path.clone(), @@ -3048,7 +3048,7 @@ async fn thread_session_state_from_thread_fork_response( ); let mut session = thread_session_state_from_thread_response( &response.thread.id, - crate::windows_sandbox::host_from_environments(response.thread.environments.as_deref()), + response.thread.environments.as_deref(), response.thread.forked_from_id.clone(), response.thread.name.clone(), response.thread.path.clone(), @@ -3101,7 +3101,7 @@ fn display_permission_profile_from_thread_response( )] async fn thread_session_state_from_thread_response( thread_id: &str, - windows_sandbox_host: crate::app::WindowsSandboxHost, + environments: Option<&[codex_app_server_protocol::ThreadEnvironment]>, forked_from_id: Option, thread_name: Option, rollout_path: Option, @@ -3134,7 +3134,10 @@ async fn thread_session_state_from_thread_response( read_only_history: false, thread_settings_confirmed: true, daybreak_enabled: false, - windows_sandbox_host, + windows_sandbox_host: crate::windows_sandbox::host_from_environments(environments), + primary_environment_cwd: crate::session_state::primary_environment_cwd( + environments.unwrap_or_default(), + ), thread_id, forked_from_id, fork_parent_title: None, @@ -5046,7 +5049,11 @@ mod tests { disabled_plugin_ids: Vec::new(), thread: codex_app_server_protocol::Thread { originator: None, - environments: None, + environments: Some(vec![codex_app_server_protocol::ThreadEnvironment { + environment_id: "remote".into(), + cwd: LegacyAppPathString::from_string("/executor/work"), + runtime_workspace_roots: Vec::new(), + }]), id: thread_id.to_string(), extra: None, session_id: ThreadId::new().to_string(), @@ -5345,7 +5352,7 @@ mod tests { let session = thread_session_state_from_thread_response( &thread_id.to_string(), - crate::app::WindowsSandboxHost::Local, + /*environments*/ None, /*forked_from_id*/ None, Some("restore".to_string()), /*rollout_path*/ None, @@ -5381,7 +5388,7 @@ mod tests { let session = thread_session_state_from_thread_response( &thread_id.to_string(), - crate::app::WindowsSandboxHost::Local, + /*environments*/ None, Some(forked_from_id.to_string()), Some("restore".to_string()), /*rollout_path*/ None, diff --git a/codex-rs/tui/src/app_server_session/rollout_history.rs b/codex-rs/tui/src/app_server_session/rollout_history.rs index 7ed5abcbe8..381be916c8 100644 --- a/codex-rs/tui/src/app_server_session/rollout_history.rs +++ b/codex-rs/tui/src/app_server_session/rollout_history.rs @@ -78,7 +78,7 @@ impl AppServerSession { } let mut session = thread_session_state_from_thread_response( &thread.id, - crate::windows_sandbox::host_from_environments(thread.environments.as_deref()), + thread.environments.as_deref(), thread.forked_from_id.clone(), thread.name.clone(), thread.path.clone(), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c2a4e0ad16..c1167400c9 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -744,6 +744,7 @@ pub(crate) struct ChatWidget { current_rollout_path: Option, // Current working directory (if known) current_cwd: Option, + primary_environment_cwd: Option, // App-server-backed command runner for status-line workspace metadata lookups. workspace_command_runner: Option, // Instruction source files loaded for the current session, supplied by app-server. diff --git a/codex-rs/tui/src/chatwidget/constructor.rs b/codex-rs/tui/src/chatwidget/constructor.rs index 9e204985d2..8db981c03f 100644 --- a/codex-rs/tui/src/chatwidget/constructor.rs +++ b/codex-rs/tui/src/chatwidget/constructor.rs @@ -276,6 +276,7 @@ impl ChatWidget { feedback, current_rollout_path: None, current_cwd, + primary_environment_cwd: None, workspace_command_runner, instruction_source_paths: Vec::new(), session_network_proxy: None, diff --git a/codex-rs/tui/src/chatwidget/session_flow.rs b/codex-rs/tui/src/chatwidget/session_flow.rs index 51074a14bb..ac84bff791 100644 --- a/codex-rs/tui/src/chatwidget/session_flow.rs +++ b/codex-rs/tui/src/chatwidget/session_flow.rs @@ -142,6 +142,8 @@ impl ChatWidget { self.update_collaboration_mode_indicator(); self.forked_from = session.forked_from_id; self.current_rollout_path = session.rollout_path.clone(); + self.primary_environment_cwd + .clone_from(&session.primary_environment_cwd); self.current_cwd = Some(session.cwd.to_path_buf()); self.config.cwd = session.cwd.clone(); self.config.model_provider_id = session.model_provider_id.clone(); diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs index df5a6c462e..9d40377898 100644 --- a/codex-rs/tui/src/chatwidget/settings.rs +++ b/codex-rs/tui/src/chatwidget/settings.rs @@ -529,6 +529,10 @@ impl ChatWidget { fn apply_thread_settings(&mut self, mut settings: ThreadSettings) { self.invalidate_permission_discovery(); let cwd_changed = self.config.cwd != settings.cwd; + if let Some(environments) = &settings.environments { + self.primary_environment_cwd = + crate::session_state::primary_environment_cwd(environments); + } self.apply_thread_settings_cwd(settings.cwd.clone()); self.config.model_provider_id = settings.model_provider.clone(); self.set_daybreak_enabled(self.daybreak_enabled); diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index 779007871a..8d6590494b 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -509,6 +509,31 @@ impl ChatWidget { .unwrap_or(self.config.cwd.as_path()) } + pub(super) fn status_line_workspace_cwd(&self) -> &Path { + self.primary_environment_cwd + .as_ref() + .map(|cwd| Path::new(cwd.path.as_str())) + .unwrap_or_else(|| self.status_line_cwd()) + } + + fn workspace_directory_display(&self) -> String { + let cwd = self.status_line_workspace_cwd(); + // Only a server-local path on a local connection shares the TUI's home. + if self + .remote_connection + .as_ref() + .is_none_or(|connection| connection.is_local_daemon) + && self + .primary_environment_cwd + .as_ref() + .is_none_or(|cwd| cwd.is_server_local) + { + format_directory_display(cwd, /*max_width*/ None) + } else { + cwd.display().to_string() + } + } + /// Resolves the project root associated with `cwd`. /// /// Git repository root wins when available. Otherwise we fall back to the @@ -735,12 +760,7 @@ impl ChatWidget { StatusLineItem::Provider => self .thread_id .map(|_| format!("provider: {}", self.config.model_provider_id)), - StatusLineItem::CurrentDir => { - Some(format_directory_display( - self.status_line_cwd(), - /*max_width*/ None, - )) - } + StatusLineItem::CurrentDir => Some(self.workspace_directory_display()), StatusLineItem::ProjectRoot => self.status_line_project_root_name(), StatusLineItem::Hostname => os_host_name(), StatusLineItem::GitBranch => self.status_line_branch.clone(), @@ -909,7 +929,7 @@ impl ChatWidget { TerminalTitleItem::AppName => Some("codex".to_string()), TerminalTitleItem::Project => self.terminal_title_project_name(), TerminalTitleItem::CurrentDir => Some(Self::truncate_terminal_title_part( - format_directory_display(self.status_line_cwd(), /*max_width*/ None), + self.workspace_directory_display(), /*max_chars*/ 32, )), TerminalTitleItem::Spinner => self.terminal_title_spinner_text_at(now), diff --git a/codex-rs/tui/src/chatwidget/statusline_state.rs b/codex-rs/tui/src/chatwidget/statusline_state.rs index cdeeb5d506..852bf73c07 100644 --- a/codex-rs/tui/src/chatwidget/statusline_state.rs +++ b/codex-rs/tui/src/chatwidget/statusline_state.rs @@ -43,12 +43,18 @@ struct StatuslineStateThread { name: Option, agent: StatuslineStateAgent, model: StatuslineStateModel, + workspace: StatuslineStateWorkspace, #[serde(skip_serializing_if = "Option::is_none")] approval: Option, #[serde(skip_serializing_if = "Option::is_none")] permissions: Option, } +#[derive(Serialize)] +struct StatuslineStateWorkspace { + cwd: codex_utils_path_uri::LegacyAppPathString, +} + #[derive(Serialize)] struct StatuslineStateAgent { state: StatuslineStateRunState, @@ -242,6 +248,11 @@ impl ChatWidget { name, agent, model, + workspace: StatuslineStateWorkspace { + cwd: codex_utils_path_uri::LegacyAppPathString::from_path( + self.status_line_workspace_cwd(), + ), + }, approval, permissions, }, diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index eecf1ce06d..c2e6164232 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -51,6 +51,7 @@ pub(super) fn configured_thread_session( crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -686,6 +687,74 @@ async fn invalid_url_elicitation_is_declined() { ); } +#[tokio::test] +async fn statusline_workspace_follows_primary_executor_and_legacy_settings() { + let (mut chat, _events, _ops) = make_chatwidget_manual(Some("gpt-5.2")).await; + let id = ThreadId::new(); + chat.handle_thread_session(configured_thread_session(id)); + let mut update = thread_settings_for_test("gpt-5.2", id); + let host = update.thread_settings.cwd.to_string_lossy().into_owned(); + let remote = r"C:\executor\checkout"; + let primary = codex_app_server_protocol::ThreadEnvironment { + environment_id: "remote".into(), + cwd: codex_utils_path_uri::LegacyAppPathString::from_string(remote), + runtime_workspace_roots: Vec::new(), + }; + for (environments, expected) in [ + (Some(vec![primary]), remote), + (None, remote), + (Some(vec![]), host.as_str()), + ] { + update.thread_settings.environments = environments; + chat.handle_server_notification( + ServerNotification::ThreadSettingsUpdated(update.clone()), + /*replay_kind*/ None, + ); + let state = serde_json::to_value(chat.statusline_state()).unwrap(); + assert_eq!(state["thread"]["workspace"]["cwd"], expected); + } +} + +#[tokio::test] +async fn statusline_and_title_abbreviate_only_the_local_workspace() { + let (mut chat, _events, _ops) = make_chatwidget_manual(Some("gpt-5.2")).await; + let id = ThreadId::new(); + chat.handle_thread_session(configured_thread_session(id)); + let mut update = thread_settings_for_test("gpt-5.2", id); + let path = dirs::home_dir() + .unwrap() + .join("a-checkout-with-a-long-directory-name"); + let raw = path.display().to_string(); + let local = format_directory_display(&path, /*max_width*/ None); + for (environment_id, expected) in [ + ("remote", raw.as_str()), + (codex_exec_server::LOCAL_ENVIRONMENT_ID, local.as_str()), + ] { + update.thread_settings.environments = + Some(vec![codex_app_server_protocol::ThreadEnvironment { + environment_id: environment_id.into(), + cwd: codex_utils_path_uri::LegacyAppPathString::from_path(&path), + runtime_workspace_roots: Vec::new(), + }]); + chat.handle_server_notification( + ServerNotification::ThreadSettingsUpdated(update.clone()), + /*replay_kind*/ None, + ); + assert_eq!( + chat.status_line_value_for_item(StatusLineItem::CurrentDir) + .as_deref(), + Some(expected) + ); + assert_eq!( + chat.terminal_title_value_for_item(TerminalTitleItem::CurrentDir, Instant::now()), + Some(ChatWidget::truncate_terminal_title_part( + expected.into(), + /*max_chars*/ 32 + )) + ); + } +} + #[tokio::test] async fn thread_settings_updated_updates_visible_state_without_transcript() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await; diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index a55957ff0c..90f41e3bcc 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -631,6 +631,7 @@ async fn submission_preserves_text_elements_and_local_images() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -743,6 +744,7 @@ async fn submission_includes_configured_active_permission_profile() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -801,6 +803,7 @@ async fn submission_omits_active_permission_profile_for_legacy_snapshot() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -849,6 +852,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -952,6 +956,7 @@ async fn enter_with_only_remote_images_submits_user_turn() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1024,6 +1029,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1068,6 +1074,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1112,6 +1119,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1159,6 +1167,7 @@ async fn submission_prefers_selected_duplicate_skill_path() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 7d0ccfe612..9eb88d5034 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -1273,6 +1273,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 40f5df9347..2ca1cf1d1e 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -22,6 +22,7 @@ async fn resumed_initial_messages_render_history() { let mut configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: true, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -186,6 +187,7 @@ async fn restored_conversation_ultra_remains_selected_after_switching_to_plan() chat.handle_thread_session(crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -511,6 +513,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -586,6 +589,7 @@ async fn replayed_user_message_preserves_remote_image_urls() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -691,6 +695,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -772,6 +777,7 @@ async fn session_configured_preserves_profile_workspace_roots() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -819,6 +825,7 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -861,6 +868,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -924,6 +932,7 @@ async fn replayed_user_message_with_only_local_images_renders_history_cell() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1284,6 +1293,7 @@ async fn replayed_reasoning_item_preserves_summary_parts_and_hides_raw_reasoning chat.handle_thread_session(crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -1338,6 +1348,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() { chat.handle_thread_session(crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 2eeeeaede6..8d514f0ab8 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -1164,6 +1164,7 @@ async fn required_windows_sandbox_setup_defers_configured_initial_prompt() { chat.handle_thread_session(crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -1616,6 +1617,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure chat.handle_thread_session(crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -1672,6 +1674,7 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w chat.handle_thread_session(crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index b9275fafa2..f2353b97eb 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -18,6 +18,7 @@ fn plan_test_session(thread_id: ThreadId) -> crate::session_state::ThreadSession crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1233,6 +1234,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() { let configured = crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 28f9ec18b7..ad137a4933 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -4721,6 +4721,7 @@ async fn session_configured_clears_goal_status_footer() { chat.handle_thread_session(crate::session_state::ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), diff --git a/codex-rs/tui/src/chatwidget/tests/statusline_state_tests.rs b/codex-rs/tui/src/chatwidget/tests/statusline_state_tests.rs index 7872afe385..f38a2aac36 100644 --- a/codex-rs/tui/src/chatwidget/tests/statusline_state_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/statusline_state_tests.rs @@ -13,6 +13,7 @@ async fn statusline_state_projects_the_displayed_settings() { let mut session = configured_thread_session(id); session.model = "gpt-5.5".into(); session.thread_name = Some("Saved carts".into()); + let expected_cwd = session.cwd.to_path_buf(); chat.handle_thread_session(session); chat.local_settings.tui.status_line = Some(vec!["status".into()]); let mut model = get_available_model(&chat, "gpt-5.5"); @@ -32,6 +33,7 @@ async fn statusline_state_projects_the_displayed_settings() { "app": {"cli_version": CODEX_CLI_VERSION, "display_mode": "standard"}, "config": {"statusline_items": ["run-state"]}, "thread": { + "workspace": {"cwd": expected_cwd}, "id": id, "name": "Saved carts", "agent": {"state": "ready"}, "model": {"id": "gpt-5.5", "display_name": "Test model", "provider": "openai", "reasoning_level": "none", "service_tier": "acme-priority", "service_tier_display_name": "Turbo"}, diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index d4c584d675..1c8fc0cce6 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -626,6 +626,7 @@ fn session_configured_event(model: &str) -> ThreadSessionState { ThreadSessionState { read_only_history: false, thread_settings_confirmed: true, + primary_environment_cwd: None, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), diff --git a/codex-rs/tui/src/session_state.rs b/codex-rs/tui/src/session_state.rs index 3120ec073a..0f22e97627 100644 --- a/codex-rs/tui/src/session_state.rs +++ b/codex-rs/tui/src/session_state.rs @@ -6,11 +6,13 @@ use std::path::PathBuf; use codex_app_server_protocol::AskForApproval; +use codex_app_server_protocol::ThreadEnvironment; use codex_protocol::ThreadId; use codex_protocol::config_types::CollaborationMode; use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::PathUri; #[derive(Debug, Clone, PartialEq)] @@ -52,6 +54,8 @@ pub(crate) struct ThreadSessionState { /// when the server knows it. pub(crate) active_permission_profile: Option, pub(crate) cwd: AbsolutePathBuf, + /// Native cwd of the primary executor, which can differ from the app-server cwd. + pub(crate) primary_environment_cwd: Option, pub(crate) runtime_workspace_roots: Vec, pub(crate) instruction_source_paths: Vec, pub(crate) reasoning_effort: Option, @@ -61,6 +65,21 @@ pub(crate) struct ThreadSessionState { pub(crate) rollout_path: Option, } +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct EnvironmentCwd { + pub(crate) path: LegacyAppPathString, + pub(crate) is_server_local: bool, +} + +pub(crate) fn primary_environment_cwd( + environments: &[ThreadEnvironment], +) -> Option { + environments.first().map(|primary| EnvironmentCwd { + path: primary.cwd.clone(), + is_server_local: primary.environment_id == codex_exec_server::LOCAL_ENVIRONMENT_ID, + }) +} + impl ThreadSessionState { pub(crate) fn set_fork_parent(&mut self, parent: Option<&str>) { let parent = parent.and_then(|id| ThreadId::from_string(id).ok()); diff --git a/codex-rs/tui/src/snapshots/codex_tui__app_server_session__tests__resumed_plan_mode.snap b/codex-rs/tui/src/snapshots/codex_tui__app_server_session__tests__resumed_plan_mode.snap index 15e6c0991a..b9469d2703 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__app_server_session__tests__resumed_plan_mode.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__app_server_session__tests__resumed_plan_mode.snap @@ -4,5 +4,5 @@ expression: "crate::chatwidget::tests::helpers::render_bottom_popup(&chat, 40)" --- › Ask Codex to do anything - GPT-5.5 default · /tmp/project + GPT-5.5 default · /executor/work Plan mode (⇧tab to cycle)