Fix slash skill invoke normalization

This commit is contained in:
Yeachan-Heo
2026-04-06 09:24:06 +00:00
parent 549ad7c3af
commit 1fc5a1c457
2 changed files with 59 additions and 1 deletions

View File

@@ -2293,10 +2293,53 @@ pub fn classify_skills_slash_command(args: Option<&str>) -> SkillSlashDispatch {
Some(args) if args == "install" || args.starts_with("install ") => {
SkillSlashDispatch::Local
}
Some(args) => SkillSlashDispatch::Invoke(format!("${args}")),
Some(args) => SkillSlashDispatch::Invoke(format!("${}", args.trim_start_matches('/'))),
}
}
/// Resolve a skill invocation by validating the skill exists on disk before
/// returning the dispatch. When the skill is not found, returns `Err` with a
/// human-readable message that lists nearby skill names.
pub fn resolve_skill_invocation(
cwd: &Path,
args: Option<&str>,
) -> Result<SkillSlashDispatch, String> {
let dispatch = classify_skills_slash_command(args);
if let SkillSlashDispatch::Invoke(ref prompt) = dispatch {
// Extract the skill name from the "$skill [args]" prompt.
let skill_token = prompt
.trim_start_matches('$')
.split_whitespace()
.next()
.unwrap_or_default();
if !skill_token.is_empty() {
if let Err(error) = resolve_skill_path(cwd, skill_token) {
let mut message =
format!("Unknown skill: {skill_token} ({error})");
let roots = discover_skill_roots(cwd);
if let Ok(available) = load_skills_from_roots(&roots) {
let names: Vec<String> = available
.iter()
.filter(|s| s.shadowed_by.is_none())
.map(|s| s.name.clone())
.collect();
if !names.is_empty() {
message.push_str(&format!(
"\n Available skills: {}",
names.join(", ")
));
}
}
message.push_str(
"\n Usage: /skills [list|install <path>|help|<skill> [args]]",
);
return Err(message);
}
}
}
Ok(dispatch)
}
pub fn resolve_skill_path(cwd: &Path, skill: &str) -> std::io::Result<PathBuf> {
let requested = skill.trim().trim_start_matches('/').trim_start_matches('$');
if requested.is_empty() {
@@ -4301,6 +4344,10 @@ mod tests {
classify_skills_slash_command(Some("help overview")),
SkillSlashDispatch::Invoke("$help overview".to_string())
);
assert_eq!(
classify_skills_slash_command(Some("/test")),
SkillSlashDispatch::Invoke("$test".to_string())
);
assert_eq!(
classify_skills_slash_command(Some("install ./skill-pack")),
SkillSlashDispatch::Local

View File

@@ -7778,6 +7778,17 @@ mod tests {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["/skills".to_string(), "/test".to_string()])
.expect("/skills /test should normalize to a single skill prompt prefix"),
CliAction::Prompt {
prompt: "$test".to_string(),
model: DEFAULT_MODEL.to_string(),
output_format: CliOutputFormat::Text,
allowed_tools: None,
permission_mode: crate::default_permission_mode(),
}
);
let error = parse_args(&["/status".to_string()])
.expect_err("/status should remain REPL-only when invoked directly");
assert!(error.contains("interactive-only"));