// ── Slash commands ────────────────────────────────────────────────────────── // Commands are loaded dynamically from GET /api/commands (Hermes COMMAND_REGISTRY). // Tier-2 Agent commands and passthrough handlers are added client-side. // Each command either runs locally or is forwarded as a message to the agent. let COMMANDS=[]; // Loaded async via loadCommands() // Map Hermes passthrough command names to their fn. // These commands are forwarded to the agent as-is. const _PASSTHROUGH=['retry','undo','title','branch','stop','background','btw', 'queue','status','profile','resume','snapshot','rollback','provider', 'yolo','reasoning','fast','voice','reload','reload-mcp','cron','browser', 'plugins','insights','platforms','debug','update','image','inbox']; function _fnFor(name){ if(name==='help'||name==='commands') return cmdHelp; if(name==='clear') return cmdClear; if(name==='compact'||name==='compress') return cmdCompact; if(name==='model') return cmdModel; if(name==='workspace') return cmdWorkspace; if(name==='new') return cmdNew; if(name==='usage') return cmdUsage; if(name==='theme') return cmdTheme; if(name==='skills') return cmdSkills; if(name==='personality') return cmdPersonality; if(_PASSTHROUGH.includes(name)) return cmdPassthrough; // Fallback: passthrough unknown commands so new Hermes commands work without JS changes return cmdPassthrough; } /** * Fetch commands from Hermes COMMAND_REGISTRY and merge with WebUI-specific commands. * Called once at boot time. */ async function loadCommands(){ try{ const data=await api('/api/commands'); if(data.error) throw new Error(data.error); const cats=data.categories||{}; // Flatten all categories into COMMANDS const merged=[]; for(const [catName,cmds] of Object.entries(cats)){ for(const c of cmds){ merged.push({name:c.name, desc:c.desc, arg:c.arg||'(none)', aliases:c.aliases||[], fn:_fnFor(c.name)}); } } // ── Tier-2 Domain Agents (WebUI-specific, override API entries) ── // Dedup: remove any API entries that would clash with Tier-2 agents const _agentNames=['sunflower','lotus','forget-me-not','iris','ivy', 'dandelion','root','back','inbox']; // Remove API entries for agent names (they may already be in the registry // from the API if agents registered themselves as commands there) const filtered=merged.filter(c=>!_agentNames.includes(c.name)); // Add Tier-2 agents (these override any API entries of the same name) filtered.push( {name:'sunflower', desc:'🌻 Finance, Wealth & Subscriptions', fn:cmdAgent, arg:'message'}, {name:'lotus', desc:'🪷 Health, Fitness & Recovery', fn:cmdAgent, arg:'message'}, {name:'forget-me-not', desc:'🌼 Calendar, Time & Social', fn:cmdAgent, arg:'message'}, {name:'iris', desc:'⚜️ Career, Learning & Focus', fn:cmdAgent, arg:'message'}, {name:'ivy', desc:'🌿 Smart Home & Environment', fn:cmdAgent, arg:'message'}, {name:'dandelion', desc:'🛡 Communication Triage & Gatekeeping',fn:cmdAgent, arg:'message'}, {name:'root', desc:'🌳 DevOps, Logs & System Health', fn:cmdAgent, arg:'message'}, {name:'back', desc:'🌹 Return to Rose (orchestrator)', fn:cmdAgent, arg:'message'}, ); COMMANDS=filtered; }catch(e){ console.warn('[commands] Failed to load from API, using fallback:',e.message); // Fallback: empty — user can still type commands manually COMMANDS=[]; } } function parseCommand(text){ if(!text.startsWith('/'))return null; const parts=text.slice(1).split(/\s+/); const name=parts[0].toLowerCase(); const args=parts.slice(1).join(' ').trim(); return {name,args}; } function executeCommand(text){ const parsed=parseCommand(text); if(!parsed)return false; const cmd=COMMANDS.find(c=>c.name===parsed.name); if(!cmd)return false; cmd.fn(parsed.args); return true; } function getMatchingCommands(prefix){ const q=prefix.toLowerCase(); return COMMANDS.filter(c=>{ if(c.name.startsWith(q)) return true; // Also match aliases if(c.aliases&&c.aliases.some(a=>a.startsWith(q))) return true; return false; }); } // ── Generic passthrough: send command text directly to agent ──────────── function cmdPassthrough(args){ const parsed=parseCommand($('msg').value); if(!parsed)return; // Forward the raw command to the agent as a regular message $('msg').value=$('msg').value; // keep as-is send(); } // ── Command handlers ──────────────────────────────────────────────────── function cmdHelp(){ // Infer categories from command names (backwards-compatible with hardcoded categories) const categories={'Session':[],'Configuration':[],'Tools & Skills':[],'Info':[],'Agents':[]}; COMMANDS.forEach(c=>{ let cat='Info'; if(['new','clear','compact','compress','retry','undo','title','branch', 'stop','background','btw','queue','status','profile','resume', 'snapshot','rollback'].includes(c.name)) cat='Session'; else if(['model','provider','personality','workspace','theme','yolo', 'reasoning','fast','voice','reload','reload-mcp'].includes(c.name)) cat='Configuration'; else if(['skills','cron','browser','plugins'].includes(c.name)) cat='Tools & Skills'; else if(['sunflower','lotus','forget-me-not','iris','ivy','dandelion', 'root','back','inbox'].includes(c.name)) cat='Agents'; if(!categories[cat])categories[cat]=[]; categories[cat].push(c); }); const lines=[]; for(const [cat,cmds] of Object.entries(categories)){ if(!cmds.length)continue; lines.push(`\n**${cat}**`); cmds.forEach(c=>{ const usage=c.arg&&c.arg!=='(none)'?` <${c.arg}>`:''; lines.push(` /${c.name}${usage} — ${c.desc}`); }); } const msg={role:'assistant',content:'Available commands:\n'+lines.join('\n')}; S.messages.push(msg); renderMessages(); showToast('Type / to see commands'); } function cmdClear(){ if(!S.session)return; S.messages=[];S.toolCalls=[]; clearLiveToolCards(); renderMessages(); $('emptyState').style.display=''; showToast(t('conversation_cleared')); } async function cmdModel(args){ if(!args){showToast('Usage: /model ');return;} const sel=$('modelSelect'); if(!sel)return; const q=args.toLowerCase(); let match=null; for(const opt of sel.options){ if(opt.value.toLowerCase().includes(q)||opt.textContent.toLowerCase().includes(q)){ match=opt.value;break; } } if(!match){showToast('No model matching "'+args+'"');return;} sel.value=match; await sel.onchange(); showToast(t('switched_to')+match); } async function cmdWorkspace(args){ if(!args){showToast('Usage: /workspace ');return;} try{ const data=await api('/api/workspaces'); const q=args.toLowerCase(); const ws=(data.workspaces||[]).find(w=> (w.name||'').toLowerCase().includes(q)||w.path.toLowerCase().includes(q) ); if(!ws){showToast('No workspace matching "'+args+'"');return;} if(typeof switchToWorkspace==='function') await switchToWorkspace(ws.path, ws.name||ws.path); else showToast(t('switched_workspace')+(ws.name||ws.path)); }catch(e){showToast(t('workspace_switch_failed')+e.message);} } async function cmdNew(){ await newSession(); await renderSessionList(); $('msg').focus(); showToast(t('new_session')); } function cmdCompact(){ $('msg').value='Please compress and summarize the conversation context to free up space.'; send(); showToast(t('compressing')); } async function cmdUsage(){ const next=!window._showTokenUsage; window._showTokenUsage=next; try{ await api('/api/settings',{method:'POST',body:JSON.stringify({show_token_usage:next})}); }catch(e){} const cb=$('settingsShowTokenUsage'); if(cb) cb.checked=next; renderMessages(); showToast(next?t('token_usage_on'):t('token_usage_off')); } async function cmdTheme(args){ const themes=['system','dark','light','slate','solarized','monokai','nord','oled']; if(!args||!themes.includes(args.toLowerCase())){ showToast('Themes: '+themes.join(' | ')); return; } const themeName=args.toLowerCase(); localStorage.setItem('hermes-theme',themeName); _applyTheme(themeName); try{await api('/api/settings',{method:'POST',body:JSON.stringify({theme:themeName})});}catch(e){} const sel=$('settingsTheme'); if(sel)sel.value=themeName; showToast(t('theme_set')+themeName); } async function cmdSkills(args){ try{ const data = await api('/api/skills'); let skills = data.skills || []; if(args){ const q = args.toLowerCase(); skills = skills.filter(s => (s.name||'').toLowerCase().includes(q) || (s.description||'').toLowerCase().includes(q) || (s.category||'').toLowerCase().includes(q) ); } if(!skills.length){ const msg = {role:'assistant', content: args ? `No skills matching "${args}".` : 'No skills found.'}; S.messages.push(msg); renderMessages(); return; } const byCategory = {}; skills.forEach(s => { const cat = s.category || 'General'; if(!byCategory[cat]) byCategory[cat] = []; byCategory[cat].push(s); }); const lines = []; for(const [cat, items] of Object.entries(byCategory).sort()){ lines.push(`**${cat}**`); items.forEach(s => { const desc = s.description ? ` — ${s.description.slice(0,80)}${s.description.length>80?'...':''}` : ''; lines.push(` \`${s.name}\`${desc}`); }); lines.push(''); } const header = args ? `Skills matching "${args}" (${skills.length}):\n\n` : `Available skills (${skills.length}):\n\n`; S.messages.push({role:'assistant', content: header + lines.join('\n')}); renderMessages(); }catch(e){ showToast('Failed to load skills: '+e.message); } } async function cmdPersonality(args){ if(!S.session){showToast(t('no_active_session'));return;} if(!args){ try{ const data=await api('/api/personalities'); if(!data.personalities||!data.personalities.length){ showToast(t('no_personalities')); return; } const list=data.personalities.map(p=>` **${p.name}**${p.description?' — '+p.description:''}`).join('\n'); S.messages.push({role:'assistant',content:t('available_personalities')+'\n\n'+list+'\n\nSwitch with: /personality '}); renderMessages(); }catch(e){showToast(t('personalities_load_failed'));} return; } const name=args.trim(); if(name.toLowerCase()==='none'||name.toLowerCase()==='default'||name.toLowerCase()==='clear'){ try{ await api('/api/personality/set',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,name:''})}); showToast(t('personality_cleared')); }catch(e){showToast(t('failed_colon')+e.message);} return; } try{ const res=await api('/api/personality/set',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,name})}); showToast(t('personality_set')+name); }catch(e){showToast(t('failed_colon')+e.message);} } // ── Tier-2 Agent Command Handler ──────────────────────────────────────── const AGENT_INFO={ 'sunflower': {emoji:'🌻', name:'Sunflower', file:'sunflower/soul.md', domain:'Finance, Wealth & Subscriptions'}, 'lotus': {emoji:'🪷', name:'Lotus', file:'lotus/soul.md', domain:'Health, Fitness & Recovery'}, 'forget-me-not': {emoji:'🌼', name:'Forget-me-not',file:'forget-me-not/soul.md', domain:'Calendar, Time & Social'}, 'iris': {emoji:'⚜️', name:'Iris', file:'iris/soul.md', domain:'Career, Learning & Focus'}, 'ivy': {emoji:'🌿', name:'Ivy', file:'ivy/soul.md', domain:'Smart Home & Environment'}, 'dandelion': {emoji:'🛡', name:'Dandelion', file:'dandelion/soul.md', domain:'Communication Triage & Gatekeeping'}, 'root': {emoji:'🌳', name:'Root', file:'root/soul.md', domain:'DevOps, Logs & System Health'}, 'back': {emoji:'🌹', name:'Rose', file:'rose/soul.md', domain:'Orchestrator (return from agent)'}, }; function cmdAgent(args){ const parsed=parseCommand($('msg').value); if(!parsed)return; const agentKey=parsed.name; const info=AGENT_INFO[agentKey]; if(!info){showToast('Unknown agent: '+agentKey);return;} const userMsg=args||''; const contextMsg=`[Agent Switch: ${info.emoji} ${info.name}]\nLoad ~/.hermes/agents/${info.file} and handle this request as ${info.name} (${info.domain}).${userMsg?'\n\nUser message: '+userMsg:''}`; $('msg').value=contextMsg; send(); } // ── Autocomplete dropdown ─────────────────────────────────────────────── let _cmdSelectedIdx=-1; function showCmdDropdown(matches){ const dd=$('cmdDropdown'); if(!dd)return; dd.innerHTML=''; _cmdSelectedIdx=-1; for(let i=0;i${esc(c.arg)}`:''; el.innerHTML=`
/${esc(c.name)}${usage}
${esc(c.desc)}
`; el.onmousedown=(e)=>{ e.preventDefault(); $('msg').value='/'+c.name+(c.arg&&c.arg!=='(none)'?' ':''); hideCmdDropdown(); $('msg').focus(); }; dd.appendChild(el); } dd.classList.add('open'); } function hideCmdDropdown(){ const dd=$('cmdDropdown'); if(dd)dd.classList.remove('open'); _cmdSelectedIdx=-1; } function navigateCmdDropdown(dir){ const dd=$('cmdDropdown'); if(!dd)return; const items=dd.querySelectorAll('.cmd-item'); if(!items.length)return; items.forEach(el=>el.classList.remove('selected')); _cmdSelectedIdx+=dir; if(_cmdSelectedIdx<0)_cmdSelectedIdx=items.length-1; if(_cmdSelectedIdx>=items.length)_cmdSelectedIdx=0; items[_cmdSelectedIdx].classList.add('selected'); } function selectCmdDropdownItem(){ const dd=$('cmdDropdown'); if(!dd)return; const items=dd.querySelectorAll('.cmd-item'); if(_cmdSelectedIdx>=0&&_cmdSelectedIdx{}}); } else if(items.length===1){ items[0].onmousedown({preventDefault:()=>{}}); } hideCmdDropdown(); }