let _currentPanel = 'chat'; let _skillsData = null; // cached skills list async function switchPanel(name) { _currentPanel = name; // Update nav tabs document.querySelectorAll('.nav-tab').forEach(t => t.classList.toggle('active', t.dataset.panel === name)); // Update panel views document.querySelectorAll('.panel-view').forEach(p => p.classList.remove('active')); const panelEl = $('panel' + name.charAt(0).toUpperCase() + name.slice(1)); if (panelEl) panelEl.classList.add('active'); // Lazy-load panel data if (name === 'tasks') await loadCrons(); if (name === 'skills') await loadSkills(); if (name === 'memory') await loadMemory(); if (name === 'workspaces') await loadWorkspacesPanel(); if (name === 'profiles') await loadProfilesPanel(); if (name === 'todos') loadTodos(); } // ── Cron panel ── async function loadCrons() { const box = $('cronList'); try { const data = await api('/api/crons'); if (!data.jobs || !data.jobs.length) { box.innerHTML = `
${esc(t('cron_no_jobs'))}
`; return; } box.innerHTML = ''; for (const job of data.jobs) { const item = document.createElement('div'); item.className = 'cron-item'; item.id = 'cron-' + job.id; const statusClass = job.enabled === false ? 'disabled' : job.state === 'paused' ? 'paused' : job.last_status === 'error' ? 'error' : 'active'; const statusLabel = job.enabled === false ? t('cron_status_off') : job.state === 'paused' ? t('cron_status_paused') : job.last_status === 'error' ? t('cron_status_error') : t('cron_status_active'); const nextRun = job.next_run_at ? new Date(job.next_run_at).toLocaleString() : t('not_available'); const lastRun = job.last_run_at ? new Date(job.last_run_at).toLocaleString() : t('never'); item.innerHTML = `
${esc(job.name)} ${statusLabel}
${li('clock',12)} ${esc(job.schedule_display || job.schedule?.expression || '')}  |  ${esc(t('cron_next'))}: ${esc(nextRun)}  |  ${esc(t('cron_last'))}: ${esc(lastRun)}
${esc((job.prompt||'').slice(0,300))}${(job.prompt||'').length>300?'…':''}
${job.state==='paused' ? `` : ``}
${esc(t('cron_last_output'))}
${esc(t('loading'))}
`; box.appendChild(item); // Eagerly load last output for visible items loadCronOutput(job.id); } } catch(e) { box.innerHTML = `
${esc(t('error_prefix'))}${esc(e.message)}
`; } } let _cronSelectedSkills=[]; let _cronSkillsCache=null; function toggleCronForm(){ const form=$('cronCreateForm'); if(!form)return; const open=form.style.display!=='none'; form.style.display=open?'none':''; if(!open){ $('cronFormName').value=''; $('cronFormSchedule').value=''; $('cronFormPrompt').value=''; $('cronFormDeliver').value='local'; $('cronFormError').style.display='none'; _cronSelectedSkills=[]; _renderCronSkillTags(); const search=$('cronFormSkillSearch'); if(search)search.value=''; // Always re-fetch skills to avoid stale cache _cronSkillsCache=null; api('/api/skills').then(d=>{_cronSkillsCache=d.skills||[];}).catch(()=>{}); $('cronFormName').focus(); } } function _renderCronSkillTags(){ const wrap=$('cronFormSkillTags'); if(!wrap)return; wrap.innerHTML=''; for(const name of _cronSelectedSkills){ const tag=document.createElement('span'); tag.className='skill-tag'; tag.dataset.skill=name; const rm=document.createElement('span'); rm.className='remove-tag';rm.textContent='×'; rm.onclick=()=>{_cronSelectedSkills=_cronSelectedSkills.filter(s=>s!==name);tag.remove();}; tag.appendChild(document.createTextNode(name)); tag.appendChild(rm); wrap.appendChild(tag); } } // Skill search input handler (function(){ const setup=()=>{ const search=$('cronFormSkillSearch'); const dropdown=$('cronFormSkillDropdown'); if(!search||!dropdown)return; search.oninput=()=>{ const q=search.value.trim().toLowerCase(); if(!q||!_cronSkillsCache){dropdown.style.display='none';return;} const matches=_cronSkillsCache.filter(s=> !_cronSelectedSkills.includes(s.name)&& (s.name.toLowerCase().includes(q)||(s.category||'').toLowerCase().includes(q)) ).slice(0,8); if(!matches.length){dropdown.style.display='none';return;} dropdown.innerHTML=''; for(const s of matches){ const opt=document.createElement('div'); opt.className='skill-opt'; opt.textContent=s.name+(s.category?' ('+s.category+')':''); opt.onclick=()=>{ _cronSelectedSkills.push(s.name); _renderCronSkillTags(); search.value=''; dropdown.style.display='none'; }; dropdown.appendChild(opt); } dropdown.style.display=''; }; search.onblur=()=>setTimeout(()=>{dropdown.style.display='none';},150); }; if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',setup); else setTimeout(setup,0); })(); async function submitCronCreate(){ const name=$('cronFormName').value.trim(); const schedule=$('cronFormSchedule').value.trim(); const prompt=$('cronFormPrompt').value.trim(); const deliver=$('cronFormDeliver').value; const errEl=$('cronFormError'); errEl.style.display='none'; if(!schedule){errEl.textContent=t('cron_schedule_required_example');errEl.style.display='';return;} if(!prompt){errEl.textContent=t('cron_prompt_required');errEl.style.display='';return;} try{ const body={schedule,prompt,deliver}; if(name)body.name=name; if(_cronSelectedSkills.length)body.skills=_cronSelectedSkills; await api('/api/crons/create',{method:'POST',body:JSON.stringify(body)}); toggleCronForm(); showToast(t('cron_job_created')); await loadCrons(); }catch(e){ errEl.textContent=t('error_prefix')+e.message;errEl.style.display=''; } } function _cronOutputSnippet(content) { // Extract the response body from a cron output .md file const lines = content.split('\n'); const responseIdx = lines.findIndex(l => l.startsWith('## Response') || l.startsWith('# Response')); const body = (responseIdx >= 0 ? lines.slice(responseIdx + 1) : lines).join('\n').trim(); return body.slice(0, 600) || '(empty)'; } async function loadCronOutput(jobId) { try { const data = await api(`/api/crons/output?job_id=${encodeURIComponent(jobId)}&limit=1`); const el = $('cron-out-text-' + jobId); if (!el) return; if (!data.outputs || !data.outputs.length) { el.textContent = t('cron_no_runs_yet'); return; } const out = data.outputs[0]; const ts = out.filename.replace('.md','').replace(/_/g,' '); el.textContent = ts + '\n\n' + _cronOutputSnippet(out.content); } catch(e) { /* ignore */ } } async function loadCronHistory(jobId, btn) { const histEl = $('cron-history-' + jobId); if (!histEl) return; // Toggle: if already open, close it if (histEl.style.display !== 'none') { histEl.style.display = 'none'; if (btn) btn.textContent = t('cron_all_runs'); return; } if (btn) btn.textContent = t('loading'); try { const data = await api(`/api/crons/output?job_id=${encodeURIComponent(jobId)}&limit=20`); if (!data.outputs || !data.outputs.length) { histEl.innerHTML = `
${esc(t('cron_no_runs_yet'))}
`; } else { histEl.innerHTML = data.outputs.map((out, i) => { const ts = out.filename.replace('.md','').replace(/_/g,' '); const snippet = _cronOutputSnippet(out.content); const id = `cron-hist-run-${jobId}-${i}`; return `
${esc(ts)}
`; }).join(''); } histEl.style.display = ''; if (btn) btn.textContent = t('cron_hide_runs'); } catch(e) { if (btn) btn.textContent = t('cron_all_runs'); } } function toggleCron(id) { const body = $('cron-body-' + id); if (body) body.classList.toggle('open'); } async function cronRun(id) { try { await api('/api/crons/run', {method:'POST', body: JSON.stringify({job_id: id})}); showToast(t('cron_job_triggered')); setTimeout(() => loadCronOutput(id), 5000); } catch(e) { showToast(t('failed_colon') + e.message, 4000); } } async function cronPause(id) { try { await api('/api/crons/pause', {method:'POST', body: JSON.stringify({job_id: id})}); showToast(t('cron_job_paused')); await loadCrons(); } catch(e) { showToast(t('failed_colon') + e.message, 4000); } } async function cronResume(id) { try { await api('/api/crons/resume', {method:'POST', body: JSON.stringify({job_id: id})}); showToast(t('cron_job_resumed')); await loadCrons(); } catch(e) { showToast(t('failed_colon') + e.message, 4000); } } function cronEditOpen(id, job) { const form = $('cron-edit-' + id); if (!form) return; $('cron-edit-name-' + id).value = job.name || ''; $('cron-edit-schedule-' + id).value = job.schedule_display || (job.schedule && job.schedule.expression) || job.schedule || ''; $('cron-edit-prompt-' + id).value = job.prompt || ''; const errEl = $('cron-edit-err-' + id); if (errEl) errEl.style.display = 'none'; form.style.display = ''; } function cronEditClose(id) { const form = $('cron-edit-' + id); if (form) form.style.display = 'none'; } async function cronEditSave(id) { const name = $('cron-edit-name-' + id).value.trim(); const schedule = $('cron-edit-schedule-' + id).value.trim(); const prompt = $('cron-edit-prompt-' + id).value.trim(); const errEl = $('cron-edit-err-' + id); if (!schedule) { errEl.textContent = t('cron_schedule_required'); errEl.style.display = ''; return; } if (!prompt) { errEl.textContent = t('cron_prompt_required'); errEl.style.display = ''; return; } try { const updates = {job_id: id, schedule, prompt}; if (name) updates.name = name; await api('/api/crons/update', {method:'POST', body: JSON.stringify(updates)}); showToast(t('cron_job_updated')); await loadCrons(); } catch(e) { errEl.textContent = t('error_prefix') + e.message; errEl.style.display = ''; } } async function cronDelete(id) { const _delCron=await showConfirmDialog({title:t('cron_delete_confirm_title'),message:t('cron_delete_confirm_message'),confirmLabel:t('delete_title'),danger:true,focusCancel:true}); if(!_delCron) return; try { await api('/api/crons/delete', {method:'POST', body: JSON.stringify({job_id: id})}); showToast(t('cron_job_deleted')); await loadCrons(); } catch(e) { showToast(t('delete_failed') + e.message, 4000); } } function loadTodos() { const panel = $('todoPanel'); if (!panel) return; const sourceMessages = (S.session && Array.isArray(S.session.messages) && S.session.messages.length) ? S.session.messages : S.messages; // Parse the most recent todo state from message history let todos = []; for (let i = sourceMessages.length - 1; i >= 0; i--) { const m = sourceMessages[i]; if (m && m.role === 'tool') { try { const d = JSON.parse(typeof m.content === 'string' ? m.content : JSON.stringify(m.content)); if (d && Array.isArray(d.todos) && d.todos.length) { todos = d.todos; break; } } catch(e) {} } } if (!todos.length) { panel.innerHTML = `
${esc(t('todos_no_active'))}
`; return; } const statusIcon = {pending:li('square',14), in_progress:li('loader',14), completed:li('check',14), cancelled:li('x',14)}; const statusColor = {pending:'var(--muted)', in_progress:'var(--blue)', completed:'rgba(100,200,100,.8)', cancelled:'rgba(200,100,100,.5)'}; panel.innerHTML = todos.map(t => `
${statusIcon[t.status]||li('square',14)}
${esc(t.content)}
${esc(t.id)} · ${esc(t.status)}
`).join(''); } async function clearConversation() { if(!S.session) return; const _clrMsg=await showConfirmDialog({title:t('clear_conversation_title'),message:t('clear_conversation_message'),confirmLabel:t('clear'),danger:true,focusCancel:true}); if(!_clrMsg) return; try { const data = await api('/api/session/clear', {method:'POST', body: JSON.stringify({session_id: S.session.session_id})}); S.session = data.session; S.messages = []; S.toolCalls = []; syncTopbar(); renderMessages(); showToast(t('conversation_cleared')); } catch(e) { setStatus(t('clear_failed') + e.message); } } // ── Skills panel ── async function loadSkills() { if (_skillsData) { renderSkills(_skillsData); return; } const box = $('skillsList'); try { const data = await api('/api/skills'); _skillsData = data.skills || []; renderSkills(_skillsData); } catch(e) { box.innerHTML = `
Error: ${esc(e.message)}
`; } } function renderSkills(skills) { const query = ($('skillsSearch').value || '').toLowerCase(); const filtered = query ? skills.filter(s => (s.name||'').toLowerCase().includes(query) || (s.description||'').toLowerCase().includes(query) || (s.category||'').toLowerCase().includes(query) ) : skills; // Group by category const cats = {}; for (const s of filtered) { const cat = s.category || '(general)'; if (!cats[cat]) cats[cat] = []; cats[cat].push(s); } const box = $('skillsList'); box.innerHTML = ''; if (!filtered.length) { box.innerHTML = `
${esc(t('skills_no_match'))}
`; return; } for (const [cat, items] of Object.entries(cats).sort()) { const sec = document.createElement('div'); sec.className = 'skills-category'; sec.innerHTML = `
${li('folder',12)} ${esc(cat)} (${items.length})
`; for (const skill of items.sort((a,b) => a.name.localeCompare(b.name))) { const el = document.createElement('div'); el.className = 'skill-item'; el.innerHTML = `${esc(skill.name)}${esc(skill.description||'')}`; el.onclick = () => openSkill(skill.name, el); sec.appendChild(el); } box.appendChild(sec); } } function filterSkills() { if (_skillsData) renderSkills(_skillsData); } async function openSkill(name, el) { // Highlight active skill document.querySelectorAll('.skill-item').forEach(e => e.classList.remove('active')); if (el) el.classList.add('active'); try { const data = await api(`/api/skills/content?name=${encodeURIComponent(name)}`); // Show skill content in right panel preview $('previewPathText').textContent = name + '.md'; $('previewBadge').textContent = 'skill'; $('previewBadge').className = 'preview-badge md'; showPreview('md'); let html = renderMd(data.content || '(no content)'); // Render linked files section if present const lf = data.linked_files || {}; const categories = Object.entries(lf).filter(([,files]) => files && files.length > 0); if (categories.length) { html += `
${esc(t('linked_files'))}
`; for (const [cat, files] of categories) { html += `

${esc(cat)}

`; for (const f of files) { html += `${esc(f)}`; } html += '
'; } html += '
'; } $('previewMd').innerHTML = html; // Wire linked-file clicks via data attributes (avoids inline JS XSS with apostrophes) $('previewMd').querySelectorAll('.skill-linked-file').forEach(a=>{ a.addEventListener('click',e=>{e.preventDefault();openSkillFile(a.dataset.skillName,a.dataset.skillFile);}); }); $('previewArea').classList.add('visible'); $('fileTree').style.display = 'none'; } catch(e) { setStatus(t('skill_load_failed') + e.message); } } async function openSkillFile(skillName, filePath) { try { const data = await api(`/api/skills/content?name=${encodeURIComponent(skillName)}&file=${encodeURIComponent(filePath)}`); $('previewPathText').textContent = skillName + ' / ' + filePath; $('previewBadge').textContent = filePath.split('.').pop() || 'file'; $('previewBadge').className = 'preview-badge code'; const ext = filePath.split('.').pop() || ''; if (['md','markdown'].includes(ext)) { showPreview('md'); $('previewMd').innerHTML = renderMd(data.content || ''); } else { showPreview('code'); $('previewCode').textContent = data.content || ''; requestAnimationFrame(() => highlightCode()); } } catch(e) { setStatus(t('skill_file_load_failed') + e.message); } } // ── Skill create/edit form ── let _editingSkillName = null; function toggleSkillForm(prefillName, prefillCategory, prefillContent) { const form = $('skillCreateForm'); if (!form) return; const open = form.style.display !== 'none'; if (open) { form.style.display = 'none'; _editingSkillName = null; return; } $('skillFormName').value = prefillName || ''; $('skillFormCategory').value = prefillCategory || ''; $('skillFormContent').value = prefillContent || ''; $('skillFormError').style.display = 'none'; _editingSkillName = prefillName || null; form.style.display = ''; $('skillFormName').focus(); } async function submitSkillSave() { const name = ($('skillFormName').value||'').trim().toLowerCase().replace(/\s+/g, '-'); const category = ($('skillFormCategory').value||'').trim(); const content = $('skillFormContent').value; const errEl = $('skillFormError'); errEl.style.display = 'none'; if (!name) { errEl.textContent = t('skill_name_required'); errEl.style.display = ''; return; } if (!content.trim()) { errEl.textContent = t('content_required'); errEl.style.display = ''; return; } try { await api('/api/skills/save', {method:'POST', body: JSON.stringify({name, category: category||undefined, content})}); showToast(_editingSkillName ? t('skill_updated') : t('skill_created')); _skillsData = null; _cronSkillsCache = null; toggleSkillForm(); await loadSkills(); } catch(e) { errEl.textContent = t('error_prefix') + e.message; errEl.style.display = ''; } } // ── Memory inline edit ── let _memoryData = null; function toggleMemoryEdit() { const form = $('memoryEditForm'); if (!form) return; const open = form.style.display !== 'none'; if (open) { form.style.display = 'none'; return; } $('memEditSection').textContent = t('memory_notes_label'); $('memEditContent').value = _memoryData ? (_memoryData.memory || '') : ''; $('memEditError').style.display = 'none'; form.style.display = ''; } function closeMemoryEdit() { const form = $('memoryEditForm'); if (form) form.style.display = 'none'; } async function submitMemorySave() { const content = $('memEditContent').value; const errEl = $('memEditError'); errEl.style.display = 'none'; try { await api('/api/memory/write', {method:'POST', body: JSON.stringify({section: 'memory', content})}); showToast(t('memory_saved')); closeMemoryEdit(); await loadMemory(true); } catch(e) { errEl.textContent = t('error_prefix') + e.message; errEl.style.display = ''; } } // ── Workspace management ── let _workspaceList = []; // cached from /api/workspaces function getWorkspaceFriendlyName(path){ // Look up the friendly name from the workspace list cache, fallback to last path segment if(_workspaceList && _workspaceList.length){ const match=_workspaceList.find(w=>w.path===path); if(match && match.name) return match.name; } return path.split('/').filter(Boolean).pop()||path; } function syncWorkspaceDisplays(){ const hasSession=!!(S.session&&S.session.workspace); const ws=hasSession?S.session.workspace:''; const label=hasSession?getWorkspaceFriendlyName(ws):t('no_workspace'); const sidebarName=$('sidebarWsName'); const sidebarPath=$('sidebarWsPath'); if(sidebarName) sidebarName.textContent=label; if(sidebarPath) sidebarPath.textContent=ws; const composerChip=$('composerWorkspaceChip'); const composerLabel=$('composerWorkspaceLabel'); const composerDropdown=$('composerWsDropdown'); if(!hasSession && composerDropdown) composerDropdown.classList.remove('open'); if(composerLabel) composerLabel.textContent=label; if(composerChip){ composerChip.disabled=!hasSession; composerChip.title=hasSession?ws:t('no_workspace'); composerChip.classList.toggle('active',!!(composerDropdown&&composerDropdown.classList.contains('open'))); } } async function loadWorkspaceList(){ try{ const data = await api('/api/workspaces'); _workspaceList = data.workspaces || []; syncWorkspaceDisplays(); return data; }catch(e){ return {workspaces:[], last:''}; } } function _renderWorkspaceAction(label, meta, iconSvg, onClick){ const opt=document.createElement('div'); opt.className='ws-opt ws-opt-action'; opt.innerHTML=`${iconSvg}${esc(label)}${meta?`${esc(meta)}`:''}`; opt.onclick=onClick; return opt; } function _positionComposerWsDropdown(){ const dd=$('composerWsDropdown'); const chip=$('composerWorkspaceChip'); const footer=document.querySelector('.composer-footer'); if(!dd||!chip||!footer)return; const chipRect=chip.getBoundingClientRect(); const footerRect=footer.getBoundingClientRect(); let left=chipRect.left-footerRect.left; const maxLeft=Math.max(0, footer.clientWidth-dd.offsetWidth); left=Math.max(0, Math.min(left, maxLeft)); dd.style.left=`${left}px`; } function _positionProfileDropdown(){ const dd=$('profileDropdown'); const chip=$('profileChip'); const footer=document.querySelector('.composer-footer'); if(!dd||!chip||!footer)return; const chipRect=chip.getBoundingClientRect(); const footerRect=footer.getBoundingClientRect(); let left=chipRect.left-footerRect.left; const maxLeft=Math.max(0, footer.clientWidth-dd.offsetWidth); left=Math.max(0, Math.min(left, maxLeft)); dd.style.left=`${left}px`; } function renderWorkspaceDropdownInto(dd, workspaces, currentWs){ if(!dd)return; dd.innerHTML=''; for(const w of workspaces){ const opt=document.createElement('div'); opt.className='ws-opt'+(w.path===currentWs?' active':''); opt.innerHTML=`${esc(w.name)}${esc(w.path)}`; opt.onclick=()=>switchToWorkspace(w.path,w.name); dd.appendChild(opt); } dd.appendChild(document.createElement('div')).className='ws-divider'; dd.appendChild(_renderWorkspaceAction( t('workspace_choose_path'), t('workspace_choose_path_meta'), li('folder',12), ()=>promptWorkspacePath() )); const div=document.createElement('div');div.className='ws-divider';dd.appendChild(div); dd.appendChild(_renderWorkspaceAction( t('workspace_manage'), t('workspace_manage_meta'), li('settings',12), ()=>{closeWsDropdown();mobileSwitchPanel('workspaces');} )); } function toggleWsDropdown(){ const dd=$('wsDropdown'); if(!dd)return; const open=dd.classList.contains('open'); if(open){closeWsDropdown();} else{ closeProfileDropdown(); // close profile dropdown if open loadWorkspaceList().then(data=>{ renderWorkspaceDropdownInto(dd, data.workspaces, S.session?S.session.workspace:''); dd.classList.add('open'); }); } } function toggleComposerWsDropdown(){ const dd=$('composerWsDropdown'); const chip=$('composerWorkspaceChip'); if(!dd||!chip||chip.disabled)return; const open=dd.classList.contains('open'); if(open){closeWsDropdown();} else{ closeProfileDropdown(); if(typeof closeModelDropdown==='function') closeModelDropdown(); loadWorkspaceList().then(data=>{ renderWorkspaceDropdownInto(dd, data.workspaces, S.session?S.session.workspace:''); dd.classList.add('open'); _positionComposerWsDropdown(); chip.classList.add('active'); }); } } function closeWsDropdown(){ const dd=$('wsDropdown'); const composerDd=$('composerWsDropdown'); const composerChip=$('composerWorkspaceChip'); if(dd)dd.classList.remove('open'); if(composerDd)composerDd.classList.remove('open'); if(composerChip)composerChip.classList.remove('active'); } document.addEventListener('click',e=>{ if( !e.target.closest('#composerWorkspaceChip') && !e.target.closest('#composerWsDropdown') ) closeWsDropdown(); }); window.addEventListener('resize',()=>{ const dd=$('composerWsDropdown'); if(dd&&dd.classList.contains('open')) _positionComposerWsDropdown(); }); async function loadWorkspacesPanel(){ const panel=$('workspacesPanel'); if(!panel)return; const data=await loadWorkspaceList(); renderWorkspacesPanel(data.workspaces); } function renderWorkspacesPanel(workspaces){ const panel=$('workspacesPanel'); panel.innerHTML=''; for(const w of workspaces){ const row=document.createElement('div');row.className='ws-row'; row.innerHTML=`
${esc(w.name)}
${esc(w.path)}
`; panel.appendChild(row); } const addRow=document.createElement('div');addRow.className='ws-add-row'; addRow.innerHTML=` `; panel.appendChild(addRow); const hint=document.createElement('div'); hint.style.cssText='font-size:11px;color:var(--muted);padding:4px 0 8px'; hint.textContent=t('workspace_paths_validated_hint'); panel.appendChild(hint); } async function addWorkspace(){ const input=$('wsAddInput'); const path=(input?input.value:'').trim(); if(!path)return; try{ const data=await api('/api/workspaces/add',{method:'POST',body:JSON.stringify({path})}); _workspaceList=data.workspaces; renderWorkspacesPanel(data.workspaces); if(input)input.value=''; showToast(t('workspace_added')); }catch(e){setStatus(t('add_failed')+e.message);} } async function removeWorkspace(path){ const _rmWs=await showConfirmDialog({title:t('workspace_remove_confirm_title'),message:t('workspace_remove_confirm_message',path),confirmLabel:t('remove'),danger:true,focusCancel:true}); if(!_rmWs) return; try{ const data=await api('/api/workspaces/remove',{method:'POST',body:JSON.stringify({path})}); _workspaceList=data.workspaces; renderWorkspacesPanel(data.workspaces); showToast(t('workspace_removed')); }catch(e){setStatus(t('remove_failed')+e.message);} } async function promptWorkspacePath(){ if(!S.session)return; const value=await showPromptDialog({ title:t('workspace_switch_prompt_title'), message:t('workspace_switch_prompt_message'), confirmLabel:t('workspace_switch_prompt_confirm'), placeholder:t('workspace_switch_prompt_placeholder'), value:S.session.workspace||'' }); const path=(value||'').trim(); if(!path)return; try{ const data=await api('/api/workspaces/add',{method:'POST',body:JSON.stringify({path})}); _workspaceList=data.workspaces||[]; const target=_workspaceList[_workspaceList.length-1]; if(!target) throw new Error(t('workspace_not_added')); await switchToWorkspace(target.path,target.name); }catch(e){ if(String(e.message||'').includes('Workspace already in list')){ showToast(t('workspace_already_saved')); return; } showToast(t('workspace_switch_failed')+e.message); } } async function switchToWorkspace(path,name){ if(!S.session)return; if(S.busy){ showToast(t('workspace_busy_switch')); return; } if(typeof _previewDirty!=='undefined'&&_previewDirty){ const discard=await showConfirmDialog({ title:t('discard_file_edits_title'), message:t('discard_file_edits_message'), confirmLabel:t('discard'), danger:true }); if(!discard)return; if(typeof cancelEditMode==='function')cancelEditMode(); if(typeof clearPreview==='function')clearPreview(); } try{ closeWsDropdown(); await api('/api/session/update',{method:'POST',body:JSON.stringify({ session_id:S.session.session_id, workspace:path, model:S.session.model })}); S.session.workspace=path; syncTopbar(); await loadDir('.'); showToast(t('workspace_switched_to',name||getWorkspaceFriendlyName(path))); }catch(e){setStatus(t('switch_failed')+e.message);} } // ── Profile panel + dropdown ── let _profilesCache = null; async function loadProfilesPanel() { const panel = $('profilesPanel'); if (!panel) return; try { const data = await api('/api/profiles'); _profilesCache = data; panel.innerHTML = ''; if (!data.profiles || !data.profiles.length) { panel.innerHTML = `
${esc(t('profiles_no_profiles'))}
`; return; } for (const p of data.profiles) { const card = document.createElement('div'); card.className = 'profile-card'; const meta = []; if (p.model) meta.push(p.model.split('/').pop()); if (p.provider) meta.push(p.provider); if (p.skill_count) meta.push(t('profile_skill_count', p.skill_count)); if (p.has_env) meta.push(t('profile_api_keys_configured')); const gwDot = p.gateway_running ? `` : ``; const isActive = p.name === data.active; const activeBadge = isActive ? `${esc(t('profile_active'))}` : ''; card.innerHTML = `
${gwDot}${esc(p.name)}${p.is_default ? ' (default)' : ''}${activeBadge}
${meta.length ? `
${esc(meta.join(' \u00b7 '))}
` : `
${esc(t('profile_no_configuration'))}
`}
${!isActive ? `` : ''} ${!p.is_default ? `` : ''}
`; panel.appendChild(card); } } catch (e) { panel.innerHTML = `
Error: ${esc(e.message)}
`; } } function renderProfileDropdown(data) { const dd = $('profileDropdown'); if (!dd) return; dd.innerHTML = ''; const profiles = data.profiles || []; const active = data.active || 'default'; for (const p of profiles) { const opt = document.createElement('div'); opt.className = 'profile-opt' + (p.name === active ? ' active' : ''); const meta = []; if (p.model) meta.push(p.model.split('/').pop()); if (p.skill_count) meta.push(t('profile_skill_count', p.skill_count)); const gwDot = ``; const checkmark = p.name === active ? ' ' : ''; opt.innerHTML = `
${gwDot}${esc(p.name)}${p.is_default ? ' (default)' : ''}${checkmark}
` + (meta.length ? `
${esc(meta.join(' \u00b7 '))}
` : ''); opt.onclick = async () => { closeProfileDropdown(); if (p.name === active) return; await switchToProfile(p.name); }; dd.appendChild(opt); } // Divider + Manage link const div = document.createElement('div'); div.className = 'ws-divider'; dd.appendChild(div); const mgmt = document.createElement('div'); mgmt.className = 'profile-opt ws-manage'; mgmt.innerHTML = `${li('settings',12)} ${esc(t('manage_profiles'))}`; mgmt.onclick = () => { closeProfileDropdown(); mobileSwitchPanel('profiles'); }; dd.appendChild(mgmt); } function toggleProfileDropdown() { const dd = $('profileDropdown'); if (!dd) return; if (dd.classList.contains('open')) { closeProfileDropdown(); return; } closeWsDropdown(); // close workspace dropdown if open if(typeof closeModelDropdown==='function') closeModelDropdown(); api('/api/profiles').then(data => { renderProfileDropdown(data); dd.classList.add('open'); _positionProfileDropdown(); const chip=$('profileChip'); if(chip) chip.classList.add('active'); }).catch(e => { showToast(t('profiles_load_failed')); }); } function closeProfileDropdown() { const dd = $('profileDropdown'); if (dd) dd.classList.remove('open'); const chip=$('profileChip'); if(chip) chip.classList.remove('active'); } document.addEventListener('click', e => { if (!e.target.closest('#profileChipWrap') && !e.target.closest('#profileDropdown')) closeProfileDropdown(); }); window.addEventListener('resize',()=>{ const dd=$('profileDropdown'); if(dd&&dd.classList.contains('open')) _positionProfileDropdown(); }); async function switchToProfile(name) { if (S.busy) { showToast(t('profiles_busy_switch')); return; } // Determine whether the current session has any messages. // A session with messages is "in progress" and belongs to the current profile — // we must not retag it. We'll start a fresh session for the new profile instead. const sessionInProgress = S.session && S.messages && S.messages.length > 0; try { const data = await api('/api/profile/switch', { method: 'POST', body: JSON.stringify({ name }) }); S.activeProfile = data.active || name; // ── Model ────────────────────────────────────────────────────────────── localStorage.removeItem('hermes-webui-model'); _skillsData = null; await populateModelDropdown(); if (data.default_model) { const sel = $('modelSelect'); const resolved = _applyModelToDropdown(data.default_model, sel); const modelToUse = resolved || data.default_model; S._pendingProfileModel = modelToUse; // Only patch the in-memory session model if we're NOT about to replace the session if (S.session && !sessionInProgress) { S.session.model = modelToUse; } } // ── Workspace ────────────────────────────────────────────────────────── _workspaceList = null; await loadWorkspaceList(); if (data.default_workspace) { // Always store the profile default for new sessions S._profileDefaultWorkspace = data.default_workspace; if (S.session && !sessionInProgress) { // Empty session (no messages yet) — safe to update it in place try { await api('/api/session/update', { method: 'POST', body: JSON.stringify({ session_id: S.session.session_id, workspace: data.default_workspace, model: S.session.model, })}); S.session.workspace = data.default_workspace; } catch (_) {} } } // ── Session ──────────────────────────────────────────────────────────── _showAllProfiles = false; if (sessionInProgress) { // The current session has messages and belongs to the previous profile. // Start a new session for the new profile so nothing gets cross-tagged. await newSession(false); // Apply profile default workspace to the newly created session (fixes #424) if (S._profileDefaultWorkspace && S.session) { try { await api('/api/session/update', { method: 'POST', body: JSON.stringify({ session_id: S.session.session_id, workspace: S._profileDefaultWorkspace, model: S.session.model, })}); S.session.workspace = S._profileDefaultWorkspace; } catch (_) {} } updateWorkspaceChip(); await renderSessionList(); showToast(t('profile_switched_new_conversation', name)); } else { // No messages yet — just refresh the list and topbar in place await renderSessionList(); syncTopbar(); showToast(t('profile_switched', name)); } // ── Sidebar panels ───────────────────────────────────────────────────── if (_currentPanel === 'skills') await loadSkills(); if (_currentPanel === 'memory') await loadMemory(); if (_currentPanel === 'tasks') await loadCrons(); if (_currentPanel === 'profiles') await loadProfilesPanel(); if (_currentPanel === 'workspaces') await loadWorkspacesPanel(); } catch (e) { showToast(t('switch_failed') + e.message); } } function toggleProfileForm() { const form = $('profileCreateForm'); if (!form) return; form.style.display = form.style.display === 'none' ? '' : 'none'; if (form.style.display !== 'none') { $('profileFormName').value = ''; $('profileFormClone').checked = false; if ($('profileFormBaseUrl')) $('profileFormBaseUrl').value = ''; if ($('profileFormApiKey')) $('profileFormApiKey').value = ''; const errEl = $('profileFormError'); if (errEl) errEl.style.display = 'none'; $('profileFormName').focus(); } } async function submitProfileCreate() { const name = ($('profileFormName').value || '').trim().toLowerCase(); const cloneConfig = $('profileFormClone').checked; const errEl = $('profileFormError'); if (!name) { errEl.textContent = t('name_required'); errEl.style.display = ''; return; } if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name)) { errEl.textContent = t('profile_name_rule'); errEl.style.display = ''; return; } try { const baseUrl = (($('profileFormBaseUrl') && $('profileFormBaseUrl').value) || '').trim(); const apiKey = (($('profileFormApiKey') && $('profileFormApiKey').value) || '').trim(); if (baseUrl && !/^https?:\/\//.test(baseUrl)) { errEl.textContent = t('profile_base_url_rule'); errEl.style.display = ''; return; } const payload = { name, clone_config: cloneConfig }; if (baseUrl) payload.base_url = baseUrl; if (apiKey) payload.api_key = apiKey; await api('/api/profile/create', { method: 'POST', body: JSON.stringify(payload) }); toggleProfileForm(); await loadProfilesPanel(); showToast(t('profile_created', name)); } catch (e) { errEl.textContent = e.message || t('create_failed'); errEl.style.display = ''; } } async function deleteProfile(name) { const _delProf=await showConfirmDialog({title:t('profile_delete_confirm_title',name),message:t('profile_delete_confirm_message'),confirmLabel:t('delete_title'),danger:true,focusCancel:true}); if(!_delProf) return; try { await api('/api/profile/delete', { method: 'POST', body: JSON.stringify({ name }) }); await loadProfilesPanel(); showToast(t('profile_deleted', name)); } catch (e) { showToast(t('delete_failed') + e.message); } } // ── Memory panel ── async function loadMemory(force) { const panel = $('memoryPanel'); try { const data = await api('/api/memory'); _memoryData = data; // cache for edit form const fmtTime = ts => ts ? new Date(ts*1000).toLocaleString() : ''; panel.innerHTML = `
${li('brain',14)} ${esc(t('my_notes'))} ${fmtTime(data.memory_mtime)}
${data.memory ? `
${renderMd(data.memory)}
` : `
${esc(t('no_notes_yet'))}
`}
${li('user',14)} ${esc(t('user_profile'))} ${fmtTime(data.user_mtime)}
${data.user ? `
${renderMd(data.user)}
` : `
${esc(t('no_profile_yet'))}
`}
`; } catch(e) { panel.innerHTML = `
${esc(t('error_prefix'))}${esc(e.message)}
`; } } // Drag and drop const wrap=$('composerWrap');let dragCounter=0; document.addEventListener('dragover',e=>e.preventDefault()); document.addEventListener('dragenter',e=>{e.preventDefault();if(e.dataTransfer.types.includes('Files')){dragCounter++;wrap.classList.add('drag-over');}}); document.addEventListener('dragleave',e=>{dragCounter--;if(dragCounter<=0){dragCounter=0;wrap.classList.remove('drag-over');}}); document.addEventListener('drop',e=>{e.preventDefault();dragCounter=0;wrap.classList.remove('drag-over');const files=Array.from(e.dataTransfer.files);if(files.length){addFiles(files);$('msg').focus();}}); // ── Settings panel ─────────────────────────────────────────────────────────── let _settingsDirty = false; let _settingsThemeOnOpen = null; // track theme at open time for discard revert let _settingsSection = 'conversation'; function switchSettingsSection(name){ const section=(name==='preferences'||name==='system')?name:'conversation'; _settingsSection=section; const map={conversation:'Conversation',preferences:'Preferences',system:'System'}; ['conversation','preferences','system'].forEach(key=>{ const tab=$('settingsTab'+map[key]); const pane=$('settingsPane'+map[key]); const active=key===section; if(tab){ tab.classList.toggle('active',active); tab.setAttribute('aria-selected',active?'true':'false'); } if(pane) pane.classList.toggle('active',active); }); } function _syncHermesPanelSessionActions(){ const hasSession=!!S.session; const visibleMessages=hasSession?(S.messages||[]).filter(m=>m&&m.role&&m.role!=='tool').length:0; const title=hasSession?(S.session.title||t('untitled')):t('active_conversation_none'); const meta=$('hermesSessionMeta'); if(meta){ meta.textContent=hasSession ? t('active_conversation_meta', title, visibleMessages) : t('active_conversation_none'); } const setDisabled=(id,disabled)=>{ const el=$(id); if(!el)return; el.disabled=!!disabled; el.classList.toggle('disabled',!!disabled); }; setDisabled('btnDownload',!hasSession||visibleMessages===0); setDisabled('btnExportJSON',!hasSession); setDisabled('btnClearConvModal',!hasSession||visibleMessages===0); } function toggleSettings(){ const overlay=$('settingsOverlay'); if(!overlay) return; if(overlay.style.display==='none'){ _settingsDirty = false; _settingsThemeOnOpen = localStorage.getItem('hermes-theme') || document.documentElement.dataset.theme || 'dark'; _settingsSection = 'conversation'; overlay.style.display=''; loadSettingsPanel(); } else { _closeSettingsPanel(); } } function _resetSettingsPanelState(){ _settingsSection = 'conversation'; switchSettingsSection('conversation'); const bar=$('settingsUnsavedBar'); if(bar) bar.style.display='none'; } function _hideSettingsPanel(){ const overlay=$('settingsOverlay'); if(!overlay) return; _resetSettingsPanelState(); overlay.style.display='none'; } // Close with unsaved-changes check. If dirty, show a confirm dialog. function _closeSettingsPanel(){ if(!_settingsDirty){ // Nothing changed -- revert any live preview and close _revertSettingsPreview(); _hideSettingsPanel(); return; } // Dirty -- show inline confirm bar _showSettingsUnsavedBar(); } // Revert live DOM/localStorage to what they were when the panel opened function _revertSettingsPreview(){ if(_settingsThemeOnOpen){ localStorage.setItem('hermes-theme', _settingsThemeOnOpen); if(typeof _applyTheme==='function') _applyTheme(_settingsThemeOnOpen); else document.documentElement.dataset.theme = _settingsThemeOnOpen; } } // Show the "Unsaved changes" bar inside the settings panel function _showSettingsUnsavedBar(){ let bar = $('settingsUnsavedBar'); if(bar){ bar.style.display=''; return; } // Create it bar = document.createElement('div'); bar.id = 'settingsUnsavedBar'; bar.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:8px;background:rgba(233,69,96,.12);border:1px solid rgba(233,69,96,.3);border-radius:8px;padding:10px 14px;margin:0 0 12px;font-size:13px;'; bar.innerHTML = `${esc(t('settings_unsaved_changes'))}` + '' + `` + `` + ''; const body = document.querySelector('.settings-main') || document.querySelector('.settings-body') || document.querySelector('.settings-panel'); if(body) body.prepend(bar); } function _discardSettings(){ _revertSettingsPreview(); _settingsDirty = false; _hideSettingsPanel(); } // Mark settings as dirty whenever anything changes function _markSettingsDirty(){ _settingsDirty = true; } async function loadSettingsPanel(){ try{ const settings=await api('/api/settings'); const resolvedLanguage=(typeof resolvePreferredLocale==='function') ? resolvePreferredLocale(settings.language, localStorage.getItem('hermes-lang')) : (settings.language || localStorage.getItem('hermes-lang') || 'en'); // Keep settings modal and current page strings in sync with the resolved locale. if(typeof setLocale==='function'){ setLocale(resolvedLanguage); if(typeof applyLocaleToDOM==='function') applyLocaleToDOM(); } // Populate model dropdown from /api/models const modelSel=$('settingsModel'); if(modelSel){ modelSel.innerHTML=''; try{ const models=await api('/api/models'); for(const g of (models.groups||[])){ const og=document.createElement('optgroup'); og.label=g.provider; for(const m of g.models){ const opt=document.createElement('option'); opt.value=m.id;opt.textContent=m.label; og.appendChild(opt); } modelSel.appendChild(og); } }catch(e){} modelSel.value=settings.default_model||''; modelSel.addEventListener('change',_markSettingsDirty,{once:false}); } // Send key preference const sendKeySel=$('settingsSendKey'); if(sendKeySel){sendKeySel.value=settings.send_key||'enter';sendKeySel.addEventListener('change',_markSettingsDirty,{once:false});} // Theme preference const themeSel=$('settingsTheme'); if(themeSel){themeSel.value=settings.theme||'dark';themeSel.addEventListener('change',_markSettingsDirty,{once:false});} // Language preference — populate from LOCALES bundle const langSel=$('settingsLanguage'); if(langSel){ langSel.innerHTML=''; if(typeof LOCALES!=='undefined'){ for(const [code,bundle] of Object.entries(LOCALES)){ const opt=document.createElement('option'); opt.value=code;opt.textContent=bundle._label||code; langSel.appendChild(opt); } } langSel.value=resolvedLanguage; langSel.addEventListener('change',_markSettingsDirty,{once:false}); } const showUsageCb=$('settingsShowTokenUsage'); if(showUsageCb){showUsageCb.checked=!!settings.show_token_usage;showUsageCb.addEventListener('change',_markSettingsDirty,{once:false});} const showCliCb=$('settingsShowCliSessions'); if(showCliCb){showCliCb.checked=!!settings.show_cli_sessions;showCliCb.addEventListener('change',_markSettingsDirty,{once:false});} const syncCb=$('settingsSyncInsights'); if(syncCb){syncCb.checked=!!settings.sync_to_insights;syncCb.addEventListener('change',_markSettingsDirty,{once:false});} const updateCb=$('settingsCheckUpdates'); if(updateCb){updateCb.checked=settings.check_for_updates!==false;updateCb.addEventListener('change',_markSettingsDirty,{once:false});} const soundCb=$('settingsSoundEnabled'); if(soundCb){soundCb.checked=!!settings.sound_enabled;soundCb.addEventListener('change',_markSettingsDirty,{once:false});} const notifCb=$('settingsNotificationsEnabled'); if(notifCb){notifCb.checked=!!settings.notifications_enabled;notifCb.addEventListener('change',_markSettingsDirty,{once:false});} const bubbleCb=$('settingsBubbleLayout'); if(bubbleCb){bubbleCb.checked=!!settings.bubble_layout;bubbleCb.addEventListener('change',_markSettingsDirty,{once:false});} // Bot name const botNameField=$('settingsBotName'); if(botNameField){botNameField.value=settings.bot_name||'Hermes';botNameField.addEventListener('input',_markSettingsDirty,{once:false});} // Password field: always blank (we don't send hash back) const pwField=$('settingsPassword'); if(pwField){pwField.value='';pwField.addEventListener('input',_markSettingsDirty,{once:false});} // Show auth buttons only when auth is active try{ const authStatus=await api('/api/auth/status'); _setSettingsAuthButtonsVisible(!!authStatus.auth_enabled); }catch(e){} _syncHermesPanelSessionActions(); switchSettingsSection(_settingsSection); }catch(e){ showToast(t('settings_load_failed')+e.message); } } function _setSettingsAuthButtonsVisible(active){ const signOutBtn=$('btnSignOut'); if(signOutBtn) signOutBtn.style.display=active?'':'none'; const disableBtn=$('btnDisableAuth'); if(disableBtn) disableBtn.style.display=active?'':'none'; } function _applySavedSettingsUi(saved, body, opts){ const {sendKey,showTokenUsage,showCliSessions,theme,language}=opts; window._sendKey=sendKey||'enter'; window._showTokenUsage=showTokenUsage; window._showCliSessions=showCliSessions; window._soundEnabled=body.sound_enabled; window._notificationsEnabled=body.notifications_enabled; window._botName=body.bot_name||'Hermes'; document.body.classList.toggle('bubble-layout', !!body.bubble_layout); if(typeof applyBotName==='function') applyBotName(); if(typeof setLocale==='function') setLocale(language); if(typeof applyLocaleToDOM==='function') applyLocaleToDOM(); if(typeof startGatewaySSE==='function'){ if(showCliSessions) startGatewaySSE(); else if(typeof stopGatewaySSE==='function') stopGatewaySSE(); } _setSettingsAuthButtonsVisible(!!saved.auth_enabled); _settingsDirty=false; _settingsThemeOnOpen=theme; const bar=$('settingsUnsavedBar'); if(bar) bar.style.display='none'; renderMessages(); if(typeof syncTopbar==='function') syncTopbar(); if(typeof renderSessionList==='function') renderSessionList(); } async function saveSettings(andClose){ const model=($('settingsModel')||{}).value; const sendKey=($('settingsSendKey')||{}).value; const showTokenUsage=!!($('settingsShowTokenUsage')||{}).checked; const showCliSessions=!!($('settingsShowCliSessions')||{}).checked; const pw=($('settingsPassword')||{}).value; const theme=($('settingsTheme')||{}).value||'dark'; const language=($('settingsLanguage')||{}).value||'en'; const body={}; if(model) body.default_model=model; if(sendKey) body.send_key=sendKey; body.theme=theme; body.language=language; body.show_token_usage=showTokenUsage; body.show_cli_sessions=showCliSessions; body.sync_to_insights=!!($('settingsSyncInsights')||{}).checked; body.check_for_updates=!!($('settingsCheckUpdates')||{}).checked; body.sound_enabled=!!($('settingsSoundEnabled')||{}).checked; body.notifications_enabled=!!($('settingsNotificationsEnabled')||{}).checked; body.bubble_layout=!!($('settingsBubbleLayout')||{}).checked; document.body.classList.toggle('bubble-layout', body.bubble_layout); const botName=(($('settingsBotName')||{}).value||'').trim(); body.bot_name=botName||'Hermes'; // Password: only act if the field has content; blank = leave auth unchanged if(pw && pw.trim()){ try{ const saved=await api('/api/settings',{method:'POST',body:JSON.stringify({...body,_set_password:pw.trim()})}); _applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,language}); showToast(t(saved.auth_just_enabled?'settings_saved_pw':'settings_saved_pw_updated')); _hideSettingsPanel(); return; }catch(e){showToast(t('settings_save_failed')+e.message);return;} } try{ const saved=await api('/api/settings',{method:'POST',body:JSON.stringify(body)}); _applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,language}); showToast(t('settings_saved')); _hideSettingsPanel(); }catch(e){ showToast(t('settings_save_failed')+e.message); } } async function signOut(){ try{ await api('/api/auth/logout',{method:'POST',body:'{}'}); window.location.href='/login'; }catch(e){ showToast(t('sign_out_failed')+e.message); } } async function disableAuth(){ const _disAuth=await showConfirmDialog({title:t('disable_auth_confirm_title'),message:t('disable_auth_confirm_message'),confirmLabel:t('disable'),danger:true,focusCancel:true}); if(!_disAuth) return; try{ await api('/api/settings',{method:'POST',body:JSON.stringify({_clear_password:true})}); showToast(t('auth_disabled')); // Hide both auth buttons since auth is now off const disableBtn=$('btnDisableAuth'); if(disableBtn) disableBtn.style.display='none'; const signOutBtn=$('btnSignOut'); if(signOutBtn) signOutBtn.style.display='none'; }catch(e){ showToast(t('disable_auth_failed')+e.message); } } // Close settings on overlay click (not panel click) -- with unsaved-changes check document.addEventListener('click',e=>{ const overlay=$('settingsOverlay'); if(overlay&&e.target===overlay) _closeSettingsPanel(); }); // ── Cron completion alerts ──────────────────────────────────────────────────── let _cronPollSince=Date.now()/1000; // track from page load let _cronPollTimer=null; let _cronUnreadCount=0; function startCronPolling(){ if(_cronPollTimer) return; _cronPollTimer=setInterval(async()=>{ if(document.hidden) return; // don't poll when tab is in background try{ const data=await api(`/api/crons/recent?since=${_cronPollSince}`); if(data.completions&&data.completions.length>0){ for(const c of data.completions){ showToast(t('cron_completion_status', c.name, c.status==='error' ? t('status_failed') : t('status_completed')),4000); _cronPollSince=Math.max(_cronPollSince,c.completed_at); } _cronUnreadCount+=data.completions.length; updateCronBadge(); } }catch(e){} },30000); } function updateCronBadge(){ const tab=document.querySelector('.nav-tab[data-panel="tasks"]'); if(!tab) return; let badge=tab.querySelector('.cron-badge'); if(_cronUnreadCount>0){ if(!badge){ badge=document.createElement('span'); badge.className='cron-badge'; tab.style.position='relative'; tab.appendChild(badge); } badge.textContent=_cronUnreadCount>9?'9+':_cronUnreadCount; badge.style.display=''; }else if(badge){ badge.style.display='none'; } } // Clear cron badge when Tasks tab is opened const _origSwitchPanel=switchPanel; switchPanel=async function(name){ if(name==='tasks'){_cronUnreadCount=0;updateCronBadge();} return _origSwitchPanel(name); }; // Start polling on page load startCronPolling(); // ── Background agent error tracking ────────────────────────────────────────── const _backgroundErrors=[]; // {session_id, title, message, ts} function trackBackgroundError(sessionId, title, message){ // Only track if user is NOT currently viewing this session if(S.session&&S.session.session_id===sessionId) return; _backgroundErrors.push({session_id:sessionId, title:title||t('untitled'), message, ts:Date.now()}); showErrorBanner(); } function showErrorBanner(){ let banner=$('bgErrorBanner'); if(!banner){ banner=document.createElement('div'); banner.id='bgErrorBanner'; banner.className='bg-error-banner'; const msgs=document.querySelector('.messages'); if(msgs) msgs.parentNode.insertBefore(banner,msgs); else document.body.appendChild(banner); } const latest=_backgroundErrors[0]; // FIFO: show oldest (first) error if(!latest){banner.style.display='none';return;} const count=_backgroundErrors.length; const msg=count>1?t('bg_error_multi',count):t('bg_error_single',latest.title); banner.innerHTML=`\u26a0 ${esc(msg)}
`; banner.style.display=''; } function navigateToErrorSession(){ const latest=_backgroundErrors.shift(); // FIFO: show oldest error first if(latest){ loadSession(latest.session_id);renderSessionList(); } if(_backgroundErrors.length===0) dismissErrorBanner(); else showErrorBanner(); } function dismissErrorBanner(){ _backgroundErrors.length=0; const banner=$('bgErrorBanner'); if(banner) banner.style.display='none'; } // Event wiring