fix(#3968): autocomplete skill names for /use
This commit is contained in:
parent
a023650330
commit
7d925a324a
3 changed files with 69 additions and 0 deletions
|
|
@ -100,6 +100,8 @@ let _slashModelCache=null;
|
|||
let _slashModelCachePromise=null;
|
||||
let _slashPersonalityCache=null;
|
||||
let _slashPersonalityCachePromise=null;
|
||||
let _slashSkillCache=null;
|
||||
let _slashSkillCachePromise=null;
|
||||
let _agentCommandCache=null;
|
||||
let _agentCommandCachePromise=null;
|
||||
|
||||
|
|
@ -117,6 +119,7 @@ function _invalidateSlashModelCache(){
|
|||
// define a window global — see tests/test_cli_only_slash_commands.py.
|
||||
if(typeof window!=='undefined'){
|
||||
window._invalidateSlashModelCache=_invalidateSlashModelCache;
|
||||
window.invalidateSlashSkillCaches=invalidateSlashSkillCaches;
|
||||
}
|
||||
|
||||
function _normalizeSlashSubArg(value){
|
||||
|
|
@ -198,10 +201,43 @@ async function _loadSlashPersonalitySubArgs(force=false){
|
|||
return _slashPersonalityCachePromise;
|
||||
}
|
||||
|
||||
async function _loadSlashSkillSubArgs(force=false){
|
||||
if(_slashSkillCache&&!force) return _slashSkillCache;
|
||||
if(_slashSkillCachePromise&&!force) return _slashSkillCachePromise;
|
||||
_slashSkillCachePromise=(async()=>{
|
||||
try{
|
||||
const data=await api('/api/skills');
|
||||
const values=[];
|
||||
for(const skill of (data&&data.skills)||[]){
|
||||
const name=_normalizeSlashSubArg(skill&&skill.name);
|
||||
if(name) values.push(name);
|
||||
}
|
||||
const deduped=Array.from(new Set(values)).sort((a,b)=>a.localeCompare(b));
|
||||
_slashSkillCache=deduped;
|
||||
return deduped;
|
||||
}catch(_){
|
||||
_slashSkillCache=null;
|
||||
return [];
|
||||
}finally{
|
||||
_slashSkillCachePromise=null;
|
||||
}
|
||||
})();
|
||||
return _slashSkillCachePromise;
|
||||
}
|
||||
|
||||
function invalidateSlashSkillCaches(){
|
||||
_slashSkillCache=null;
|
||||
_slashSkillCachePromise=null;
|
||||
_skillCommandCache=[];
|
||||
_skillCommandCacheReady=false;
|
||||
_skillCommandLoadPromise=null;
|
||||
}
|
||||
|
||||
function _getSlashSubArgOptions(spec){
|
||||
if(Array.isArray(spec)) return Promise.resolve(spec.slice());
|
||||
if(spec==='models') return _loadSlashModelSubArgs();
|
||||
if(spec==='personalities') return _loadSlashPersonalitySubArgs();
|
||||
if(spec==='skills') return _loadSlashSkillSubArgs();
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3860,6 +3860,7 @@ async function toggleSkill(name, currentlyEnabled) {
|
|||
const skill = _skillsData.find(s => s.name === name);
|
||||
if (skill) skill.disabled = !newEnabled;
|
||||
}
|
||||
if(typeof window!=='undefined'&&typeof window.invalidateSlashSkillCaches==='function') window.invalidateSlashSkillCaches();
|
||||
renderSkills(_skillsData || []);
|
||||
} else {
|
||||
setStatus((result && result.error) || t('skill_toggle_failed'));
|
||||
|
|
@ -4114,6 +4115,7 @@ async function saveSkillForm() {
|
|||
showToast(_editingSkillName ? t('skill_updated') : t('skill_created'));
|
||||
_skillsData = null;
|
||||
_cronSkillsCache = null;
|
||||
if(typeof window!=='undefined'&&typeof window.invalidateSlashSkillCaches==='function') window.invalidateSlashSkillCaches();
|
||||
_editingSkillName = null;
|
||||
_skillPreFormDetail = null;
|
||||
await loadSkills();
|
||||
|
|
@ -4153,6 +4155,7 @@ async function deleteCurrentSkill() {
|
|||
_skillPreFormDetail = null;
|
||||
_skillsData = null;
|
||||
_cronSkillsCache = null;
|
||||
if(typeof window!=='undefined'&&typeof window.invalidateSlashSkillCaches==='function') window.invalidateSlashSkillCaches();
|
||||
_skillMode = 'empty';
|
||||
const body = $('skillDetailBody');
|
||||
const empty = $('skillDetailEmpty');
|
||||
|
|
|
|||
|
|
@ -13,15 +13,45 @@ import pathlib
|
|||
REPO_ROOT = pathlib.Path(__file__).parent.parent
|
||||
COMMANDS_JS = (REPO_ROOT / "static" / "commands.js").read_text(encoding="utf-8")
|
||||
BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
PANELS_JS = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _function_body(source, name):
|
||||
marker = f"function {name}("
|
||||
start = source.find(marker)
|
||||
assert start != -1, f"{name} function not found"
|
||||
next_function = source.find("\nfunction ", start + len(marker))
|
||||
next_async_function = source.find("\nasync function ", start + len(marker))
|
||||
ends = [pos for pos in (next_function, next_async_function) if pos != -1]
|
||||
end = min(ends) if ends else len(source)
|
||||
return source[start:end]
|
||||
|
||||
|
||||
def test_skill_commands_are_loaded_from_api_skills_for_autocomplete():
|
||||
assert "loadSkillCommands" in COMMANDS_JS
|
||||
assert "api('/api/skills')" in COMMANDS_JS
|
||||
assert "source:'skill'" in COMMANDS_JS
|
||||
|
||||
|
||||
def test_use_command_declares_skills_subargs():
|
||||
assert "{name:'use'" in COMMANDS_JS
|
||||
assert "subArgs:'skills'" in COMMANDS_JS
|
||||
|
||||
|
||||
def test_skills_subargs_route_through_dedicated_loader():
|
||||
assert "async function _loadSlashSkillSubArgs(force=false)" in COMMANDS_JS
|
||||
assert "if(spec==='skills') return _loadSlashSkillSubArgs();" in COMMANDS_JS
|
||||
assert "if(spec==='skills') return loadSkillCommands();" not in COMMANDS_JS
|
||||
|
||||
|
||||
def test_skill_mutations_invalidate_slash_skill_caches():
|
||||
assert "function invalidateSlashSkillCaches()" in COMMANDS_JS
|
||||
assert "window.invalidateSlashSkillCaches=invalidateSlashSkillCaches;" in COMMANDS_JS
|
||||
for function_name in ("saveSkillForm", "deleteCurrentSkill", "toggleSkill"):
|
||||
assert "window.invalidateSlashSkillCaches()" in _function_body(PANELS_JS, function_name)
|
||||
|
||||
|
||||
def test_builtin_commands_take_precedence_over_skill_slug_collisions():
|
||||
# In the combined implementation, REGISTRY (agent registry + WEBUI_ONLY) wins over skills
|
||||
assert ("if(COMMANDS.some(c=>c.name===slug)) return null;" in COMMANDS_JS or
|
||||
|
|
|
|||
Loading…
Reference in a new issue