fix(commands): search skills by keyword in slash autocomplete

This commit is contained in:
MHJ committed 2026-08-11 23:40:00 +09:00
1 parent 3c7fbe3809
commit eea4f03db8
3 files changed
+55 -3

No files matched your search

+5 -2
View File
@@ -187,6 +187,7 @@ function getMatchingCommands(prefix){
const matches=COMMANDS.filter(c=>c.name.startsWith(q)).map(c=>({...c,source:'builtin'}));
const seen=new Set(matches.map(c=>c.name));
const reserved=_getReservedSlashCommandSlugs();
const bundleSlugs=new Set(_bundleCommandCache.map(bundle=>bundle.name));
for(const [name, spec] of Object.entries(SLASH_SUBARG_SOURCES)){
if(!name.startsWith(q)||seen.has(name))continue;
matches.push({
@@ -228,9 +229,11 @@ function getMatchingCommands(prefix){
}
}
for(const skill of _skillCommandCache){
if(!skill.name.startsWith(q)||seen.has(skill.name)||reserved.has(skill.name))continue;
const name=String(skill&&skill.name||'').toLowerCase();
const description=String(skill&&skill.desc||'').toLowerCase();
if((!name.includes(q)&&!description.includes(q))||seen.has(name)||reserved.has(name)||bundleSlugs.has(name))continue;
matches.push(skill);
seen.add(skill.name);
seen.add(name);
}
return matches;
}
+47
View File
@@ -215,6 +215,18 @@ def _run_commands_js(script_body: str) -> dict:
{{
name: 'plugin review',
description: 'Plugin collisions should stay hidden from slash autocomplete'
}},
{{
name: 'hermes-upgrade',
description: 'Safely upgrade and verify a Hermes installation'
}},
{{
name: 'maintenance-guide',
description: 'Plan a safe upgrade and rollback path'
}},
{{
name: 'reinstall-helper',
description: 'Recover from a failed reinstall'
}}
]
}};
@@ -352,6 +364,41 @@ def test_cli_only_slugs_reserve_skill_autocomplete_namespace():
assert "use" in result["use_names"]
def test_skill_autocomplete_matches_keyword_in_name_or_description():
result = _run_commands_js(
"""
await loadSkillCommands(true);
const upgrade = await getSlashAutocompleteMatches('/upgrade');
const reinstall = await getSlashAutocompleteMatches('/reinstall');
return {
upgrade_names: upgrade.map(item => item.name),
upgrade_sources: upgrade.map(item => item.source),
reinstall_names: reinstall.map(item => item.name),
reinstall_sources: reinstall.map(item => item.source)
};
"""
)
assert result["upgrade_names"] == ["hermes-upgrade", "maintenance-guide"]
assert result["upgrade_sources"] == ["skill", "skill"]
assert result["reinstall_names"] == ["reinstall-helper"]
assert result["reinstall_sources"] == ["skill"]
def test_skill_autocomplete_hides_keyword_match_shadowed_by_bundle():
result = _run_commands_js(
"""
await loadAgentCommandMetadata(true);
await loadBundleCommands(true);
await loadSkillCommands(true);
const matches = await getSlashAutocompleteMatches('/non-colliding');
return matches.map(item => ({ name: item.name, source: item.source }));
"""
)
assert result == []
def test_bundle_collisions_stay_hidden_until_agent_metadata_is_ready():
script = textwrap.dedent(
f"""
+3 -1
View File
@@ -63,7 +63,9 @@ def test_skill_mutations_invalidate_slash_skill_caches():
def test_builtin_commands_take_precedence_over_skill_slug_collisions():
assert "_getReservedSlashCommandSlugs" in COMMANDS_JS
assert "if(_getReservedSlashCommandSlugs().has(slug)) return null;" in COMMANDS_JS
assert "if(!skill.name.startsWith(q)||seen.has(skill.name)||reserved.has(skill.name))continue;" in COMMANDS_JS
command_matching = _function_body(COMMANDS_JS, "getMatchingCommands")
assert "reserved.has(name)" in command_matching
assert "!name.includes(q)&&!description.includes(q)" in command_matching
def test_bundle_entries_merge_before_plain_skill_entries():