From 1ea79a3c0dcb2309d3c2b1b9df067d00523dcc30 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 19 Feb 2026 15:41:02 -0800 Subject: [PATCH] Fix skills cwd canonicalization --- codex-rs/tui/src/chatwidget/skills.rs | 57 ++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/chatwidget/skills.rs b/codex-rs/tui/src/chatwidget/skills.rs index e7fadde1af..53e85802e6 100644 --- a/codex-rs/tui/src/chatwidget/skills.rs +++ b/codex-rs/tui/src/chatwidget/skills.rs @@ -144,9 +144,22 @@ impl ChatWidget { } fn skills_for_cwd(cwd: &Path, skills_entries: &[SkillsListEntry]) -> Vec { + let cwd_canonical = dunce::canonicalize(cwd).ok(); skills_entries .iter() - .find(|entry| entry.cwd.as_path() == cwd) + .find(|entry| { + if entry.cwd.as_path() == cwd { + return true; + } + + let Some(cwd_canonical) = cwd_canonical.as_ref() else { + return false; + }; + let Ok(entry_canonical) = dunce::canonicalize(&entry.cwd) else { + return false; + }; + entry_canonical == *cwd_canonical + }) .map(|entry| entry.skills.clone()) .unwrap_or_default() } @@ -449,3 +462,45 @@ fn app_id_from_path(path: &str) -> Option<&str> { fn is_app_or_mcp_path(path: &str) -> bool { path.starts_with("app://") || path.starts_with("mcp://") } + +#[cfg(test)] +mod tests { + use super::skills_for_cwd; + use codex_core::protocol::SkillMetadata as ProtocolSkillMetadata; + use codex_core::protocol::SkillScope; + use codex_core::protocol::SkillsListEntry; + use pretty_assertions::assert_eq; + use std::path::PathBuf; + use tempfile::tempdir; + + #[cfg(unix)] + #[test] + fn skills_for_cwd_matches_symlinked_paths() { + let tmp = tempdir().expect("tempdir"); + let real_dir = tmp.path().join("real"); + let link_dir = tmp.path().join("link"); + std::fs::create_dir_all(&real_dir).expect("create real dir"); + std::os::unix::fs::symlink(&real_dir, &link_dir).expect("create symlink"); + + let skill = ProtocolSkillMetadata { + name: "demo".to_string(), + description: "desc".to_string(), + short_description: None, + interface: None, + dependencies: None, + path: PathBuf::from("/tmp/skills/demo/SKILL.md"), + scope: SkillScope::Repo, + enabled: true, + }; + + let entries = vec![SkillsListEntry { + cwd: real_dir, + skills: vec![skill], + errors: Vec::new(), + }]; + + let matched = skills_for_cwd(&link_dir, &entries); + let names = matched.iter().map(|s| s.name.as_str()).collect::>(); + assert_eq!(names, vec!["demo"]); + } +}