Phase 8: TypeScript migration, i18n rewrite, Activity Tree, Projects API, Heartbeats
This commit is contained in:
@@ -1,379 +1,400 @@
|
||||
// ── 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=[];
|
||||
(() => {
|
||||
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"
|
||||
];
|
||||
let COMMANDS = [];
|
||||
const AGENT_INFO = {
|
||||
"sunflower": { emoji: "\u{1F33B}", name: "Sunflower", file: "sunflower/soul.md", domain: "Finance, Wealth & Subscriptions" },
|
||||
"lotus": { emoji: "\u{1F9D7}", name: "Lotus", file: "lotus/soul.md", domain: "Health, Fitness & Recovery" },
|
||||
"forget-me-not": { emoji: "\u{1F33C}", name: "Forget-me-not", file: "forget-me-not/soul.md", domain: "Calendar, Time & Social" },
|
||||
"iris": { emoji: "\u2695\uFE0F", name: "Iris", file: "iris/soul.md", domain: "Career, Learning & Focus" },
|
||||
"ivy": { emoji: "\u{1F33F}", name: "Ivy", file: "ivy/soul.md", domain: "Smart Home & Environment" },
|
||||
"dandelion": { emoji: "\u{1F6E1}", name: "Dandelion", file: "dandelion/soul.md", domain: "Communication Triage & Gatekeeping" },
|
||||
"root": { emoji: "\u{1F333}", name: "Root", file: "root/soul.md", domain: "DevOps, Logs & System Health" },
|
||||
"back": { emoji: "\u{1F339}", name: "Rose", file: "rose/soul.md", domain: "Orchestrator (return from agent)" }
|
||||
};
|
||||
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;
|
||||
return cmdPassthrough;
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
async function loadCommands() {
|
||||
try {
|
||||
const data = await api("/api/commands");
|
||||
if (data.error) throw new Error(data.error);
|
||||
const cats = data.categories || {};
|
||||
const merged = [];
|
||||
for (const [, 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) });
|
||||
}
|
||||
}
|
||||
const agentNames = ["sunflower", "lotus", "forget-me-not", "iris", "ivy", "dandelion", "root", "back", "inbox"];
|
||||
const filtered = merged.filter((c) => !agentNames.includes(c.name));
|
||||
filtered.push(
|
||||
{ name: "sunflower", desc: "\u{1F33B} Finance, Wealth & Subscriptions", fn: cmdAgent, arg: "message", aliases: [] },
|
||||
{ name: "lotus", desc: "\u{1F9D7} Health, Fitness & Recovery", fn: cmdAgent, arg: "message", aliases: [] },
|
||||
{ name: "forget-me-not", desc: "\u{1F33C} Calendar, Time & Social", fn: cmdAgent, arg: "message", aliases: [] },
|
||||
{ name: "iris", desc: "\u2695\uFE0F Career, Learning & Focus", fn: cmdAgent, arg: "message", aliases: [] },
|
||||
{ name: "ivy", desc: "\u{1F33F} Smart Home & Environment", fn: cmdAgent, arg: "message", aliases: [] },
|
||||
{ name: "dandelion", desc: "\u{1F6E1} Communication Triage & Gatekeeping", fn: cmdAgent, arg: "message", aliases: [] },
|
||||
{ name: "root", desc: "\u{1F333} DevOps, Logs & System Health", fn: cmdAgent, arg: "message", aliases: [] },
|
||||
{ name: "back", desc: "\u{1F339} Return to Rose (orchestrator)", fn: cmdAgent, arg: "message", aliases: [] }
|
||||
);
|
||||
COMMANDS = filtered;
|
||||
} catch (e) {
|
||||
console.warn("[commands] Failed to load from API, using fallback:", e instanceof Error ? e.message : String(e));
|
||||
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;
|
||||
if (c.aliases && c.aliases.some((a) => a.startsWith(q))) return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
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 <model_name>');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;
|
||||
}
|
||||
function cmdPassthrough(_args) {
|
||||
const msgEl = $("msg");
|
||||
if (!msgEl) return;
|
||||
const parsed = parseCommand(msgEl.value);
|
||||
if (!parsed) return;
|
||||
send();
|
||||
}
|
||||
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 <name>');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);
|
||||
function cmdHelp() {
|
||||
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, 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}`);
|
||||
for (const [cat, cmds] of Object.entries(categories)) {
|
||||
if (!cmds.length) continue;
|
||||
lines.push(`
|
||||
**${cat}**`);
|
||||
cmds.forEach((c) => {
|
||||
const usage = c.arg && c.arg !== "(none)" ? ` <${c.arg}>` : "";
|
||||
lines.push(` /${c.name}${usage} \u2014 ${c.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')});
|
||||
const msg = { role: "assistant", content: "Available commands:\n" + lines.join("\n") };
|
||||
S.messages.push(msg);
|
||||
renderMessages();
|
||||
}catch(e){
|
||||
showToast('Failed to load skills: '+e.message);
|
||||
showToast(t("type_slash"));
|
||||
}
|
||||
}
|
||||
|
||||
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'));
|
||||
function cmdClear() {
|
||||
if (!S.session) return;
|
||||
S.messages = [];
|
||||
S.toolCalls = [];
|
||||
clearLiveToolCards();
|
||||
renderMessages();
|
||||
const emptyState = $("emptyState");
|
||||
if (emptyState) emptyState.style.display = "";
|
||||
showToast(t("conversation_cleared"));
|
||||
}
|
||||
async function cmdModel(args) {
|
||||
if (!args) {
|
||||
showToast("Usage: /model <model_name>");
|
||||
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;
|
||||
if (sel.onchange) await sel.onchange(null);
|
||||
showToast(t("switched_to") + match);
|
||||
}
|
||||
async function cmdWorkspace(args) {
|
||||
if (!args) {
|
||||
showToast("Usage: /workspace <name>");
|
||||
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;
|
||||
}
|
||||
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 <name>'});
|
||||
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 instanceof Error ? e.message : String(e)));
|
||||
}
|
||||
}
|
||||
async function cmdNew() {
|
||||
await newSession();
|
||||
await renderSessionList();
|
||||
const msgEl = $("msg");
|
||||
if (msgEl) msgEl.focus();
|
||||
showToast(t("new_session"));
|
||||
}
|
||||
function cmdCompact() {
|
||||
const msgEl = $("msg");
|
||||
if (msgEl) msgEl.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 {
|
||||
}
|
||||
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 {
|
||||
}
|
||||
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 ? ` \u2014 ${s.description.slice(0, 80)}${s.description.length > 80 ? "..." : ""}` : "";
|
||||
lines.push(` \`${s.name}\`${desc}`);
|
||||
});
|
||||
lines.push("");
|
||||
}
|
||||
const header = args ? `Skills matching "${args}" (${skills.length}):
|
||||
|
||||
` : `Available skills (${skills.length}):
|
||||
|
||||
`;
|
||||
S.messages.push({ role: "assistant", content: header + lines.join("\n") });
|
||||
renderMessages();
|
||||
}catch(e){showToast(t('personalities_load_failed'));}
|
||||
return;
|
||||
} catch (e) {
|
||||
showToast("Failed to load skills: " + (e instanceof Error ? e.message : String(e)));
|
||||
}
|
||||
}
|
||||
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;
|
||||
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 ? " \u2014 " + p.description : ""}`).join("\n");
|
||||
S.messages.push({ role: "assistant", content: t("available_personalities") + "\n\n" + list + "\n\nSwitch with: /personality <name>" });
|
||||
renderMessages();
|
||||
} catch {
|
||||
showToast(t("personalities_load_failed"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const name = args.trim();
|
||||
if (["none", "default", "clear"].includes(name.toLowerCase())) {
|
||||
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 instanceof Error ? e.message : String(e)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
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 instanceof Error ? e.message : String(e)));
|
||||
}
|
||||
}
|
||||
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<matches.length;i++){
|
||||
const c=matches[i];
|
||||
const el=document.createElement('div');
|
||||
el.className='cmd-item';
|
||||
el.dataset.idx=i;
|
||||
const usage=c.arg&&c.arg!=='(none)'?` <span class="cmd-item-arg">${esc(c.arg)}</span>`:'';
|
||||
el.innerHTML=`<div class="cmd-item-name">/${esc(c.name)}${usage}</div><div class="cmd-item-desc">${esc(c.desc)}</div>`;
|
||||
el.onmousedown=(e)=>{
|
||||
e.preventDefault();
|
||||
$('msg').value='/'+c.name+(c.arg&&c.arg!=='(none)'?' ':'');
|
||||
hideCmdDropdown();
|
||||
$('msg').focus();
|
||||
};
|
||||
dd.appendChild(el);
|
||||
function cmdAgent(_args) {
|
||||
const msgEl = $("msg");
|
||||
if (!msgEl) return;
|
||||
const parsed = parseCommand(msgEl.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}]
|
||||
Load ~/.hermes/agents/${info.file} and handle this request as ${info.name} (${info.domain}).${userMsg ? "\n\nUser message: " + userMsg : ""}`;
|
||||
msgEl.value = contextMsg;
|
||||
send();
|
||||
}
|
||||
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<items.length){
|
||||
items[_cmdSelectedIdx].onmousedown({preventDefault:()=>{}});
|
||||
} else if(items.length===1){
|
||||
items[0].onmousedown({preventDefault:()=>{}});
|
||||
let _cmdSelectedIdx = -1;
|
||||
function showCmdDropdown(matches) {
|
||||
const dd = $("cmdDropdown");
|
||||
if (!dd) return;
|
||||
dd.innerHTML = "";
|
||||
_cmdSelectedIdx = -1;
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
const c = matches[i];
|
||||
const el = document.createElement("div");
|
||||
el.className = "cmd-item";
|
||||
el.dataset.idx = String(i);
|
||||
const usage = c.arg && c.arg !== "(none)" ? ` <span class="cmd-item-arg">${esc(c.arg)}</span>` : "";
|
||||
el.innerHTML = `<div class="cmd-item-name">/${esc(c.name)}${usage}</div><div class="cmd-item-desc">${esc(c.desc)}</div>`;
|
||||
el.addEventListener("mousedown", (e) => {
|
||||
e.preventDefault();
|
||||
const msgEl2 = $("msg");
|
||||
if (msgEl2) {
|
||||
msgEl2.value = "/" + c.name + (c.arg && c.arg !== "(none)" ? " " : "");
|
||||
msgEl2.focus();
|
||||
}
|
||||
hideCmdDropdown();
|
||||
});
|
||||
dd.appendChild(el);
|
||||
}
|
||||
dd.classList.add("open");
|
||||
}
|
||||
hideCmdDropdown();
|
||||
}
|
||||
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 < items.length) {
|
||||
const item = items[_cmdSelectedIdx];
|
||||
const ev = new MouseEvent("mousedown", { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(ev, "preventDefault", { value: () => {
|
||||
} });
|
||||
item.dispatchEvent(ev);
|
||||
} else if (items.length === 1) {
|
||||
const ev = new MouseEvent("mousedown", { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(ev, "preventDefault", { value: () => {
|
||||
} });
|
||||
items[0].dispatchEvent(ev);
|
||||
}
|
||||
hideCmdDropdown();
|
||||
}
|
||||
function _applyTheme(themeName) {
|
||||
document.documentElement.dataset.theme = themeName;
|
||||
}
|
||||
window.loadCommands = loadCommands;
|
||||
})();
|
||||
//# sourceMappingURL=commands.js.map
|
||||
|
||||
Reference in New Issue
Block a user