Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 72 additions & 49 deletions codex-rs/app-server/src/request_processors/catalog_processor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use futures::StreamExt;

#[derive(Clone)]
pub(crate) struct CatalogRequestProcessor {
Expand All @@ -9,6 +10,8 @@ pub(crate) struct CatalogRequestProcessor {
pub(super) workspace_settings_cache: Arc<workspace_settings::WorkspaceSettingsCache>,
}

const SKILLS_LIST_CWD_CONCURRENCY: usize = 5;

fn skills_to_info(
skills: &[codex_core::skills::SkillMetadata],
disabled_paths: &HashSet<AbsolutePathBuf>,
Expand Down Expand Up @@ -430,56 +433,76 @@ impl CatalogRequestProcessor {
.environment_manager()
.default_environment()
.map(|environment| environment.get_filesystem());
let mut data = Vec::new();
for cwd in cwds {
let (cwd_abs, config_layer_stack) = match self.resolve_cwd_config(&cwd).await {
Ok(resolved) => resolved,
Err(message) => {
let error_path = cwd.clone();
data.push(codex_app_server_protocol::SkillsListEntry {
cwd,
skills: Vec::new(),
errors: vec![codex_app_server_protocol::SkillErrorInfo {
path: error_path,
message,
}],
});
continue;
let mut data = futures::stream::iter(cwds.into_iter().enumerate())
.map(|(index, cwd)| {
let config = &config;
let extra_roots_by_cwd = &extra_roots_by_cwd;
let fs = fs.clone();
let plugins_manager = &plugins_manager;
let skills_manager = &skills_manager;
async move {
let (cwd_abs, config_layer_stack) = match self.resolve_cwd_config(&cwd).await {
Ok(resolved) => resolved,
Err(message) => {
let error_path = cwd.clone();
return (
index,
codex_app_server_protocol::SkillsListEntry {
cwd,
skills: Vec::new(),
errors: vec![codex_app_server_protocol::SkillErrorInfo {
path: error_path,
message,
}],
},
);
}
};
let extra_roots = extra_roots_by_cwd
.get(&cwd)
.map_or(&[][..], std::vec::Vec::as_slice);
let effective_skill_roots = if workspace_codex_plugins_enabled {
let plugins_input = config.plugins_config_input();
plugins_manager
.effective_skill_roots_for_layer_stack(
&config_layer_stack,
&plugins_input,
)
.await
} else {
Vec::new()
};
let skills_input = codex_core::skills::SkillsLoadInput::new(
cwd_abs.clone(),
effective_skill_roots,
config_layer_stack,
config.bundled_skills_enabled(),
);
let outcome = skills_manager
.skills_for_cwd_with_extra_user_roots(
&skills_input,
force_reload,
extra_roots,
fs,
)
.await;
let errors = errors_to_info(&outcome.errors);
let skills = skills_to_info(&outcome.skills, &outcome.disabled_paths);
(
index,
codex_app_server_protocol::SkillsListEntry {
cwd,
skills,
errors,
},
)
}
};
let extra_roots = extra_roots_by_cwd
.get(&cwd)
.map_or(&[][..], std::vec::Vec::as_slice);
let effective_skill_roots = if workspace_codex_plugins_enabled {
let plugins_input = config.plugins_config_input();
plugins_manager
.effective_skill_roots_for_layer_stack(&config_layer_stack, &plugins_input)
.await
} else {
Vec::new()
};
let skills_input = codex_core::skills::SkillsLoadInput::new(
cwd_abs.clone(),
effective_skill_roots,
config_layer_stack,
config.bundled_skills_enabled(),
);
let outcome = skills_manager
.skills_for_cwd_with_extra_user_roots(
&skills_input,
force_reload,
extra_roots,
fs.clone(),
)
.await;
let errors = errors_to_info(&outcome.errors);
let skills = skills_to_info(&outcome.skills, &outcome.disabled_paths);
data.push(codex_app_server_protocol::SkillsListEntry {
cwd,
skills,
errors,
});
}
})
.buffer_unordered(SKILLS_LIST_CWD_CONCURRENCY)
.collect::<Vec<_>>()
.await;
data.sort_unstable_by_key(|(index, _)| *index);
let data = data.into_iter().map(|(_, entry)| entry).collect();
Ok(SkillsListResponse { data })
}

Expand Down
38 changes: 38 additions & 0 deletions codex-rs/app-server/tests/suite/v2/skills_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,44 @@ async fn skills_list_accepts_relative_cwds() -> Result<()> {
Ok(())
}

#[tokio::test]
async fn skills_list_preserves_requested_cwd_order() -> Result<()> {
let codex_home = TempDir::new()?;
let first_cwd = TempDir::new()?;
let second_cwd = TempDir::new()?;

let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;

let request_id = mcp
.send_skills_list_request(SkillsListParams {
cwds: vec![
first_cwd.path().to_path_buf(),
second_cwd.path().to_path_buf(),
],
force_reload: true,
per_cwd_extra_user_roots: None,
})
.await?;

let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let SkillsListResponse { data } = to_response(response)?;
assert_eq!(
data.iter()
.map(|entry| entry.cwd.clone())
.collect::<Vec<_>>(),
vec![
first_cwd.path().to_path_buf(),
second_cwd.path().to_path_buf(),
]
);
Ok(())
}

#[tokio::test]
async fn skills_list_ignores_per_cwd_extra_roots_for_unknown_cwd() -> Result<()> {
let codex_home = TempDir::new()?;
Expand Down
Loading