fix(chat): query expert skills via symlinks and use $ prefix for Codex

Expert skills in the chat session were derived by intersecting built-in
experts with ACP availableCommands, which caused Codex experts to never
appear since Codex does not advertise skills through ACP.

- Add `experts_list_for_agent` backend API that checks symlink status
  across all global skill dirs for the given agent type
- Replace availableCommands-based expert filtering with symlink-based
  query, making the settings page the single source of truth
- Use `$` prefix for Codex expert skills while keeping `/` for slash
  commands and other agents' experts
- Disable the expert button when no experts are linked for the agent
- Invalidate per-agent expert cache after link/unlink in settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-11 00:13:42 +08:00
parent e4eb7f67eb
commit ade59f474c
11 changed files with 187 additions and 29 deletions

View File

@@ -682,6 +682,47 @@ pub async fn experts_list() -> Result<Vec<ExpertListItem>, ExpertsError> {
Ok(out)
}
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
pub async fn experts_list_for_agent(
agent_type: AgentType,
) -> Result<Vec<ExpertListItem>, ExpertsError> {
let _ = skill_storage_spec(agent_type)
.ok_or(ExpertsError::UnsupportedAgent(agent_type))?;
let dirs = scoped_skill_dirs(agent_type, AgentSkillScope::Global, None)
.map_err(|_| ExpertsError::UnsupportedAgent(agent_type))?;
let meta_list = bundled_metadata().to_vec();
let manifest = load_manifest();
let mut out = Vec::new();
for meta in meta_list {
let central_path = expert_central_path(&meta.id);
let is_linked = dirs.iter().any(|dir| {
let candidate = dir.join(&meta.id);
classify_link(&candidate, &central_path) == ExpertLinkState::LinkedToCodeg
});
if !is_linked {
continue;
}
let installed_centrally = central_path.exists();
let user_modified = manifest
.experts
.get(&meta.id)
.map(|e| e.pending_user_review)
.unwrap_or(false);
out.push(ExpertListItem {
metadata: meta,
installed_centrally,
user_modified,
central_path: central_path.to_string_lossy().to_string(),
});
}
Ok(out)
}
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
pub async fn experts_get_install_status(
expert_id: String,

View File

@@ -376,6 +376,7 @@ mod tauri_app {
acp_commands::acp_save_agent_skill,
acp_commands::acp_delete_agent_skill,
experts_commands::experts_list,
experts_commands::experts_list_for_agent,
experts_commands::experts_get_install_status,
experts_commands::experts_link_to_agent,
experts_commands::experts_unlink_from_agent,

View File

@@ -12,6 +12,12 @@ pub struct ExpertIdParams {
pub expert_id: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentTypeOnlyParams {
pub agent_type: AgentType,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExpertAgentParams {
@@ -26,6 +32,15 @@ pub async fn experts_list() -> Result<Json<Vec<ExpertListItem>>, AppCommandError
Ok(Json(result))
}
pub async fn experts_list_for_agent(
Json(params): Json<AgentTypeOnlyParams>,
) -> Result<Json<Vec<ExpertListItem>>, AppCommandError> {
let result = experts_commands::experts_list_for_agent(params.agent_type)
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(result))
}
pub async fn experts_get_install_status(
Json(params): Json<ExpertIdParams>,
) -> Result<Json<Vec<ExpertInstallStatus>>, AppCommandError> {

View File

@@ -177,6 +177,7 @@ pub fn build_router(state: Arc<AppState>, token: String, static_dir: std::path::
.route("/acp_delete_agent_skill", post(handlers::acp::acp_delete_agent_skill))
// ─── Experts ───
.route("/experts_list", post(handlers::experts::experts_list))
.route("/experts_list_for_agent", post(handlers::experts::experts_list_for_agent))
.route("/experts_get_install_status", post(handlers::experts::experts_get_install_status))
.route("/experts_link_to_agent", post(handlers::experts::experts_link_to_agent))
.route("/experts_unlink_from_agent", post(handlers::experts::experts_unlink_from_agent))