diff --git a/codex-rs/exec-server/src/environment_toml.rs b/codex-rs/exec-server/src/environment_toml.rs index d144c65a95..e754bce01a 100644 --- a/codex-rs/exec-server/src/environment_toml.rs +++ b/codex-rs/exec-server/src/environment_toml.rs @@ -197,16 +197,10 @@ pub(crate) fn environment_provider_from_codex_home( codex_home: &Path, ) -> Result, ExecServerError> { let path = codex_home.join(ENVIRONMENTS_TOML_FILE); - if !path.try_exists().map_err(|err| { - ExecServerError::Protocol(format!( - "failed to inspect environment config `{}`: {err}", - path.display() - )) - })? { + let Some(environments) = load_environments_toml(&path)? else { return Ok(Box::new(DefaultEnvironmentProvider::from_env())); - } + }; - let environments = load_environments_toml(&path)?; Ok(Box::new(TomlEnvironmentProvider::new_with_config_dir( environments, Some(codex_home), @@ -295,20 +289,27 @@ fn validate_websocket_url(url: String) -> Result { Ok(url.to_string()) } -fn load_environments_toml(path: &Path) -> Result { - let contents = std::fs::read_to_string(path).map_err(|err| { - ExecServerError::Protocol(format!( - "failed to read environment config `{}`: {err}", - path.display() - )) - })?; +/// Returns `None` when the config is missing; other I/O and parse failures remain errors. +fn load_environments_toml(path: &Path) -> Result, ExecServerError> { + let contents = match std::fs::read_to_string(path) { + Ok(contents) => contents, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(err) => { + return Err(ExecServerError::Protocol(format!( + "failed to read environment config `{}`: {err}", + path.display() + ))); + } + }; - toml::from_str(&contents).map_err(|err| { - ExecServerError::Protocol(format!( - "failed to parse environment config `{}`: {err}", - path.display() - )) - }) + toml::from_str(&contents) + .map_err(|err| { + ExecServerError::Protocol(format!( + "failed to parse environment config `{}`: {err}", + path.display() + )) + }) + .map(Some) } mod option_duration_secs { @@ -756,7 +757,9 @@ CODEX_LOG = "debug" ) .expect("write environments.toml"); - let environments = load_environments_toml(&path).expect("environments.toml"); + let environments = load_environments_toml(&path) + .expect("environments.toml") + .expect("environments.toml should exist"); assert_eq!(environments.default.as_deref(), Some("ssh-dev")); assert_eq!(environments.include_local, Some(false)); diff --git a/codex-rs/file-search/src/lib.rs b/codex-rs/file-search/src/lib.rs index c49e57aeb9..bcad11f770 100644 --- a/codex-rs/file-search/src/lib.rs +++ b/codex-rs/file-search/src/lib.rs @@ -67,6 +67,12 @@ pub enum MatchType { Directory, } +/// Carries the entry type observed by the walker so matched paths are not restatted. +struct IndexedEntry { + full_path: Arc, + match_type: MatchType, +} + impl FileMatch { pub fn full_path(&self) -> PathBuf { self.root.join(&self.path) @@ -411,7 +417,7 @@ fn get_file_path<'a>(path: &'a Path, search_directories: &[PathBuf]) -> Option<( fn walker_worker( inner: Arc, override_matcher: Option, - injector: Injector>, + injector: Injector, ) { let Some(first_root) = inner.search_directories.first() else { let _ = inner.work_tx.send(WorkSignal::WalkComplete); @@ -463,9 +469,19 @@ fn walker_worker( return ignore::WalkState::Continue; }; if let Some((_, relative_path)) = get_file_path(path, &search_directories) { - injector.push(Arc::from(full_path), |_, cols| { - cols[0] = Utf32String::from(relative_path); - }); + let match_type = match entry.file_type() { + Some(file_type) if file_type.is_dir() => MatchType::Directory, + _ => MatchType::File, + }; + injector.push( + IndexedEntry { + full_path: Arc::from(full_path), + match_type, + }, + |_, cols| { + cols[0] = Utf32String::from(relative_path); + }, + ); } n += 1; if n >= CHECK_INTERVAL { @@ -483,7 +499,7 @@ fn walker_worker( fn matcher_worker( inner: Arc, work_rx: Receiver, - mut nucleo: Nucleo>, + mut nucleo: Nucleo, ) -> anyhow::Result<()> { const TICK_TIMEOUT_MS: u64 = 10; let config = Config::DEFAULT.match_paths(); @@ -547,7 +563,7 @@ fn matcher_worker( .take(limit) .filter_map(|match_| { let item = snapshot.get_item(match_.idx)?; - let full_path = item.data.as_ref(); + let full_path = item.data.full_path.as_ref(); let (root_idx, relative_path) = get_file_path(Path::new(full_path), &inner.search_directories)?; let indices = if let Some(indices_matcher) = indices_matcher.as_mut() { let mut idx_vec = Vec::::new(); @@ -559,15 +575,10 @@ fn matcher_worker( } else { None }; - let match_type = if Path::new(full_path).is_dir() { - MatchType::Directory - } else { - MatchType::File - }; Some(FileMatch { score: match_.score, path: PathBuf::from(relative_path), - match_type, + match_type: item.data.match_type, root: inner.search_directories[root_idx].clone(), indices, }) @@ -1012,6 +1023,29 @@ mod tests { })); } + #[cfg(unix)] + #[test] + fn run_classifies_followed_directory_symlink_as_directory() { + use std::os::unix::fs::symlink; + + let dir = tempfile::tempdir().unwrap(); + fs::create_dir(dir.path().join("guides")).unwrap(); + symlink(dir.path().join("guides"), dir.path().join("guides-link")).unwrap(); + + let results = run( + "guides-link", + vec![dir.path().to_path_buf()], + FileSearchOptions::default(), + /*cancel_flag*/ None, + ) + .expect("run ok"); + + assert!(results.matches.iter().any(|file_match| { + file_match.path == Path::new("guides-link") + && file_match.match_type == MatchType::Directory + })); + } + #[test] fn cancel_exits_run() { let dir = create_temp_tree(/*file_count*/ 200); diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 8aef050cc2..67401985cd 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -414,10 +414,6 @@ async fn connect_remote_app_server( #[cfg(unix)] async fn maybe_probe_default_daemon_socket(codex_home: &Path) -> Option { let socket_path = codex_app_server_client::app_server_control_socket_path(codex_home).ok()?; - if !socket_path.as_path().try_exists().unwrap_or(false) { - return None; - } - match tokio::time::timeout( AUTO_CONNECT_DAEMON_CONNECT_TIMEOUT, tokio::net::UnixStream::connect(socket_path.as_path()), diff --git a/codex-rs/utils/path-utils/src/lib.rs b/codex-rs/utils/path-utils/src/lib.rs index 71b0a6473d..e09042ba01 100644 --- a/codex-rs/utils/path-utils/src/lib.rs +++ b/codex-rs/utils/path-utils/src/lib.rs @@ -6,6 +6,7 @@ pub use env::is_wsl; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashSet; use std::io; +use std::io::Write; use std::path::Path; use std::path::PathBuf; use tempfile::NamedTempFile; @@ -126,8 +127,8 @@ pub fn write_atomically(write_path: &Path, contents: &str) -> io::Result<()> { ) })?; std::fs::create_dir_all(parent)?; - let tmp = NamedTempFile::new_in(parent)?; - std::fs::write(tmp.path(), contents)?; + let mut tmp = NamedTempFile::new_in(parent)?; + tmp.write_all(contents.as_bytes())?; tmp.persist(write_path)?; Ok(()) }