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
This commit is contained in:
Joe Savona
2026-11-13 23:49:59 +00:00
committed by copyberry
parent 68847438b5
commit 38cdc4d655
26 changed files with 210 additions and 18 deletions
@@ -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(),
@@ -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,
+8 -1
View File
@@ -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,
+1
View File
@@ -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,
+7
View File
@@ -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());
@@ -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,
+4
View File
@@ -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
+15 -8
View File
@@ -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<String>,
thread_name: Option<String>,
rollout_path: Option<PathBuf>,
@@ -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,
@@ -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(),
+1
View File
@@ -744,6 +744,7 @@ pub(crate) struct ChatWidget {
current_rollout_path: Option<PathBuf>,
// Current working directory (if known)
current_cwd: Option<PathBuf>,
primary_environment_cwd: Option<crate::session_state::EnvironmentCwd>,
// App-server-backed command runner for status-line workspace metadata lookups.
workspace_command_runner: Option<WorkspaceCommandRunner>,
// Instruction source files loaded for the current session, supplied by app-server.
@@ -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,
@@ -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();
+4
View File
@@ -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);
+27 -7
View File
@@ -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),
@@ -43,12 +43,18 @@ struct StatuslineStateThread {
name: Option<String>,
agent: StatuslineStateAgent,
model: StatuslineStateModel,
workspace: StatuslineStateWorkspace,
#[serde(skip_serializing_if = "Option::is_none")]
approval: Option<StatuslineStateApproval>,
#[serde(skip_serializing_if = "Option::is_none")]
permissions: Option<StatuslineStatePermissions>,
}
#[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,
},
@@ -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;
@@ -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,
@@ -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,
@@ -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(),
@@ -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(),
@@ -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,
@@ -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(),
@@ -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"},
+1
View File
@@ -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(),
+19
View File
@@ -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<ActivePermissionProfile>,
pub(crate) cwd: AbsolutePathBuf,
/// Native cwd of the primary executor, which can differ from the app-server cwd.
pub(crate) primary_environment_cwd: Option<EnvironmentCwd>,
pub(crate) runtime_workspace_roots: Vec<AbsolutePathBuf>,
pub(crate) instruction_source_paths: Vec<PathUri>,
pub(crate) reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
@@ -61,6 +65,21 @@ pub(crate) struct ThreadSessionState {
pub(crate) rollout_path: Option<PathBuf>,
}
#[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<EnvironmentCwd> {
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());
@@ -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)