ext/skills/src/selection.rs
129 lines
use std::collections::HashSet;use codex_core_skills::injection::extract_tool_mentions;use codex_protocol::user_input::UserInput;use crate::catalog::SkillAuthority;use crate::catalog::SkillCatalog;use crate::catalog::SkillCatalogEntry;use crate::catalog::SkillPackageId;const SKILL_PATH_PREFIX: &str = "skill://";pub(crate) fn collect_explicit_skill_mentions( inputs: &[UserInput], catalog: &SkillCatalog,) -> Vec<SkillCatalogEntry> { let mut selected = Vec::new(); let mut seen = HashSet::new(); let mut blocked_plain_names = HashSet::new(); for input in inputs { match input { UserInput::Skill { name, path } => { blocked_plain_names.insert(name.clone()); select_by_path(catalog, &path.to_string_lossy(), &mut seen, &mut selected); } UserInput::Mention { name, path } if path_is_skill(path) => { blocked_plain_names.insert(name.clone()); select_by_path(catalog, path, &mut seen, &mut selected); } UserInput::Text { .. } | UserInput::Image { .. } | UserInput::LocalImage { .. } => {} UserInput::Mention { .. } => {} _ => {} } } for input in inputs { let UserInput::Text { text, .. } = input else { continue; }; let mentions = extract_tool_mentions(text); for path in mentions.paths() { if path_is_skill(path) { select_by_path( catalog, normalize_skill_path(path), &mut seen, &mut selected, ); } } for name in mentions.plain_names() { if blocked_plain_names.contains(name) { continue; } if let Some(entry) = catalog .entries .iter() .find(|entry| entry.enabled && entry.name == name) { push_selected(entry, &mut seen, &mut selected); } } } selected}fn select_by_path( catalog: &SkillCatalog, path: &str, seen: &mut HashSet<SkillCatalogEntryKey>, selected: &mut Vec<SkillCatalogEntry>,) { let normalized_path = normalize_skill_path(path); for entry in catalog.entries.iter().filter(|entry| entry.enabled) { if entry_matches_path(entry, normalized_path) { push_selected(entry, seen, selected); } }}fn push_selected( entry: &SkillCatalogEntry, seen: &mut HashSet<SkillCatalogEntryKey>, selected: &mut Vec<SkillCatalogEntry>,) { let key = SkillCatalogEntryKey::from(entry); if seen.insert(key) { selected.push(entry.clone()); }}fn entry_matches_path(entry: &SkillCatalogEntry, path: &str) -> bool { entry.main_prompt.as_str() == path || entry.id.0 == path || entry .display_path .as_deref() .is_some_and(|display_path| normalize_skill_path(display_path) == path)}fn path_is_skill(path: &str) -> bool { path.starts_with(SKILL_PATH_PREFIX) || path .rsplit(['/', '\\']) .next() .is_some_and(|file_name| file_name.eq_ignore_ascii_case("SKILL.md"))}fn normalize_skill_path(path: &str) -> &str { path.strip_prefix(SKILL_PATH_PREFIX).unwrap_or(path)}#[derive(Clone, Debug, PartialEq, Eq, Hash)]struct SkillCatalogEntryKey { authority: SkillAuthority, package: SkillPackageId,}impl From<&SkillCatalogEntry> for SkillCatalogEntryKey { fn from(entry: &SkillCatalogEntry) -> Self { Self { authority: entry.authority.clone(), package: entry.id.clone(), } }}