Merge #4010 into stage-mk

This commit is contained in:
nesquena-hermes 2026-06-12 23:18:00 +00:00
commit 0eee1b85ec
3 changed files with 69 additions and 0 deletions

View file

@ -101,6 +101,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;
@ -118,6 +120,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){
@ -199,10 +202,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([]);
}

View file

@ -3865,6 +3865,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'));
@ -4119,6 +4120,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();
@ -4158,6 +4160,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');

View file

@ -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():
assert "_getReservedSlashCommandSlugs" in COMMANDS_JS
assert "if(_getReservedSlashCommandSlugs().has(slug)) return null;" in COMMANDS_JS