// ── Slash commands ────────────────────────────────────────────────────────── // Built-in commands intercepted before send(). Each command runs locally // (no round-trip to the agent) and shows feedback via toast or local message. const COMMANDS=[ // noEcho:true = action-only commands that don't produce a chat response. // Commands without noEcho get a user message echoed to the chat (#840). {name:'help', desc:t('cmd_help'), fn:cmdHelp}, {name:'clear', desc:t('cmd_clear'), fn:cmdClear, noEcho:true}, {name:'compress', desc:t('cmd_compress'), fn:cmdCompress, arg:'[focus topic]', noEcho:true}, {name:'compact', desc:t('cmd_compact_alias'), fn:cmdCompact, noEcho:true}, {name:'model', desc:t('cmd_model'), fn:cmdModel, arg:'model_name', subArgs:'models', noEcho:true}, {name:'workspace', desc:t('cmd_workspace'), fn:cmdWorkspace, arg:'name', noEcho:true}, {name:'terminal', desc:t('cmd_terminal'), fn:cmdTerminal, noEcho:true}, {name:'new', desc:t('cmd_new'), fn:cmdNew, noEcho:true}, {name:'usage', desc:t('cmd_usage'), fn:cmdUsage, noEcho:true}, {name:'theme', desc:t('cmd_theme'), fn:cmdTheme, arg:'name', noEcho:true}, {name:'personality', desc:t('cmd_personality'), fn:cmdPersonality, arg:'name', subArgs:'personalities'}, {name:'skills', desc:t('cmd_skills'), fn:cmdSkills, arg:'query'}, {name:'stop', desc:t('cmd_stop'), fn:cmdStop, noEcho:true}, {name:'goal', desc:t('cmd_goal'), fn:cmdGoal, arg:'[status|pause|resume|clear|text]', subArgs:['status','pause','resume','clear']}, {name:'queue', desc:t('cmd_queue'), fn:cmdQueue, arg:'message', noEcho:true}, {name:'interrupt', desc:t('cmd_interrupt'), fn:cmdInterrupt, arg:'message', noEcho:true}, {name:'steer', desc:t('cmd_steer'), fn:cmdSteer, arg:'message', noEcho:true}, {name:'title', desc:t('cmd_title'), fn:cmdTitle, arg:'[title]'}, {name:'retry', desc:t('cmd_retry'), fn:cmdRetry, noEcho:true}, {name:'undo', desc:t('cmd_undo'), fn:cmdUndo, noEcho:true}, {name:'btw', desc:t('cmd_btw'), fn:cmdBtw, arg:'question', noEcho:true}, {name:'background',desc:t('cmd_background'),fn:cmdBackground,arg:'prompt', noEcho:true}, {name:'status', desc:t('cmd_status'), fn:cmdStatus}, {name:'voice', desc:t('cmd_voice'), fn:cmdVoice, noEcho:true}, {name:'reasoning', desc:t('cmd_reasoning'), fn:cmdReasoning, arg:'show|hide|none|minimal|low|medium|high|xhigh', subArgs:['show','hide','none','minimal','low','medium','high','xhigh'], noEcho:true}, {name:'yolo', desc:t('cmd_yolo'), fn:cmdYolo, noEcho:true}, {name:'branch', desc:t('cmd_branch'), fn:cmdBranch, arg:'[name]', noEcho:true}, ]; const SLASH_SUBARG_SOURCES={ model:{desc:t('cmd_model'), subArgs:'models'}, personality:{desc:t('cmd_personality'), subArgs:'personalities'}, }; 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 null; const cmd=COMMANDS.find(c=>c.name===parsed.name); if(!cmd)return null; // A handler may return `false` to opt out of interception — e.g. /reasoning // with an effort level falls through so the agent's own handler sees it, // preserving the pre-existing pass-through behaviour for that subcommand. if(cmd.fn(parsed.args)===false)return null; // Return noEcho flag so send() knows whether to echo the command as a user message (#840). return {noEcho:!!cmd.noEcho}; } function getMatchingCommands(prefix){ const q=prefix.toLowerCase(); const matches=COMMANDS.filter(c=>c.name.startsWith(q)).map(c=>({...c,source:'builtin'})); const seen=new Set(matches.map(c=>c.name)); for(const [name, spec] of Object.entries(SLASH_SUBARG_SOURCES)){ if(!name.startsWith(q)||seen.has(name))continue; matches.push({ name, desc:spec.desc, arg:'name', source:'subarg-command', }); seen.add(name); } for(const skill of _skillCommandCache){ if(!skill.name.startsWith(q)||seen.has(skill.name))continue; matches.push(skill); seen.add(skill.name); } // Include agent/plugin commands from /api/commands metadata for(const cmd of (_agentCommandCache||[])){ const name=String(cmd&&cmd.name||'').toLowerCase(); if(!name.startsWith(q)||seen.has(name))continue; if(cmd.cli_only)continue; matches.push({ name, desc:String(cmd&&cmd.description||'').trim()||'Agent command', source:cmd.category==='Plugin'?'plugin':'agent', }); seen.add(name); } return matches; } let _slashModelCache=null; let _slashModelCachePromise=null; let _slashPersonalityCache=null; let _slashPersonalityCachePromise=null; let _agentCommandCache=null; let _agentCommandCachePromise=null; // Invalidate the /api/models slash-suggestion cache. Called by panels.js // after a provider is added or removed so the next /model autocomplete // rebuilds from a fresh /api/models response (#1539). Returning a function // rather than letting callers poke the module-local lets/promises directly // keeps the cache shape encapsulated to this module. function _invalidateSlashModelCache(){ _slashModelCache=null; _slashModelCachePromise=null; } // Expose on window when available. Guarded by typeof so the module is // importable in headless test contexts (vm.runInContext) that don't // define a window global — see tests/test_cli_only_slash_commands.py. if(typeof window!=='undefined'){ window._invalidateSlashModelCache=_invalidateSlashModelCache; } function _normalizeSlashSubArg(value){ return String(value||'').trim(); } function _getSlashModelSubArgsFromDom(){ const sel=$('modelSelect'); if(!sel) return []; const values=[]; for(const opt of Array.from(sel.options||[])){ const value=_normalizeSlashSubArg(opt.value||opt.textContent||''); if(value) values.push(value); } return Array.from(new Set(values)).sort((a,b)=>a.localeCompare(b)); } async function _loadSlashModelSubArgs(force=false){ const domValues=_getSlashModelSubArgsFromDom(); if(domValues.length&&!force){ _slashModelCache=domValues; return domValues; } if(_slashModelCache&&!force) return _slashModelCache; if(_slashModelCachePromise&&!force) return _slashModelCachePromise; _slashModelCachePromise=(async()=>{ try{ const data=await api('/api/models'); const values=[]; for(const group of (data&&data.groups)||[]){ for(const model of (group&&group.models)||[]){ const id=_normalizeSlashSubArg(model&&model.id); if(id) values.push(id); } // Include extra_models (the catalog tail that doesn't render as //