// ── ARC REACTOR STATUS ──────────────────────────────────────────────── let _arcOnline = false; let _arcJobs = { queued: 0, running: 0, done: 0, failed: 0 }; async function checkArcStatus() { const dot = document.getElementById('bb-arc-dot'); const sta = document.getElementById('bb-arc-status'); if (!dot || !sta) return; try { const d = await api('arc?action=status'); if (d && d.online) { _arcOnline = true; dot.className = 'bb-dot online'; const active = (d.active_jobs || 0) + (d.queued_jobs || 0); sta.textContent = active > 0 ? active + ' JOB' + (active !== 1 ? 'S' : '') : 'ONLINE'; _arcJobs = { queued: d.queued_jobs||0, running: d.running_jobs||0, done: d.jobs_done||0, failed: d.jobs_failed||0 }; } else { _arcOnline = false; dot.className = 'bb-dot offline'; sta.textContent = 'OFFLINE'; } } catch(e) { _arcOnline = false; dot.className = 'bb-dot offline'; sta.textContent = 'OFFLINE'; } } // Submit a job to the Arc Reactor and return job_id async function arcSubmitJob(type, payload, priority) { payload = payload || {}; priority = priority || 5; const d = await api('arc', { action: 'job_create', type: type, payload: payload, priority: priority }); return d.job_id || null; } // Poll a job until done or failed (max 120s), calling onProgress each tick async function arcWaitJob(jobId, onProgress) { var start = Date.now(); while (Date.now() - start < 120000) { const d = await api('arc?action=job_get&id=' + jobId); if (onProgress) onProgress(d); if (d.status === 'done') return d; if (d.status === 'failed') throw new Error(d.error || 'Job failed'); await new Promise(function(r){ setTimeout(r, 1500); }); } throw new Error('Arc Reactor job timed out'); } // ── INTEL PROTOCOL — HUD panel ──────────────────────────────────────── let _intelPollTimer = null; let _intelActiveJobs = new Set(); let _intelLastLoad = 0; async function loadIntel() { const el = document.getElementById('intel-list'); if (!el) return; _intelLastLoad = Date.now(); try { // Fetch recent research + tool_loop jobs const [resJobs, toolJobs] = await Promise.all([ api('arc?action=jobs&status=&limit=20').catch(() => []), Promise.resolve([]), ]); const jobs = Array.isArray(resJobs) ? resJobs.filter(j => ['research','tool_loop','llm'].includes(j.job_type)) : []; if (!jobs.length) { el.innerHTML = '
◈ NO INTEL JOBS
Say "research [topic]" to activate
'; stopIntelPolling(); return; } // Check for active jobs const hasActive = jobs.some(j => j.status === 'queued' || j.status === 'running'); if (hasActive) startIntelPolling(); else stopIntelPolling(); let html = ''; for (const job of jobs) { const isOpen = _intelActiveJobs.has(job.id) || job.status === 'running'; const statusClass = job.status === 'done' ? 'done' : job.status === 'failed' ? 'failed' : 'running'; const statusLabel = job.status === 'queued' ? 'QUEUED' : job.status === 'running' ? '● ACTIVE' : job.status.toUpperCase(); const typeLabel = job.job_type === 'research' ? '◈ INTEL' : job.job_type === 'tool_loop' ? '⚡ IRON' : '◈ LLM'; // Get result details if done let bodyHtml = ''; if (job.status === 'done' && job.result) { let r = job.result; if (typeof r === 'string') { try { r = JSON.parse(r); } catch(e) {} } if (typeof r === 'object') { const synthesis = (r.synthesis || r.result || r.response || '').trim(); const sources = r.sources || []; const query = r.query || r.task || ''; const provider = r.provider || ''; bodyHtml = `
`; if (provider) bodyHtml += `
PROVIDER: ${provider.toUpperCase()} · SOURCES: ${r.source_count||sources.length||'—'}
`; if (synthesis) bodyHtml += `
${escHtml(synthesis.substring(0, 1500))}${synthesis.length>1500?'\n\n[...truncated — view in admin]':''}
`; if (sources.length) { bodyHtml += '
SOURCES
'; sources.slice(0,5).forEach((s,i) => { const title = escHtml((s.title||s.url||'').substring(0,60)); const url = escHtml(s.url||''); bodyHtml += ``; }); bodyHtml += '
'; } bodyHtml += '
'; } } else if (job.status === 'running' || job.status === 'queued') { const typeMsg = job.job_type === 'research' ? 'Searching sources and extracting content...' : 'Executing tool loop...'; bodyHtml = `
${typeMsg}
`; } else if (job.status === 'failed' && job.error) { bodyHtml = `
${escHtml(job.error.substring(0,200))}
`; } const queryText = job.created_by ? job.created_by.replace('chat:', '').replace(/session.*/, '') : ''; const ts = job.created_at ? new Date(job.created_at).toLocaleTimeString() : ''; html += `
${typeLabel} #${job.id} ${escHtml((job.created_by||'').replace('chat:','').substring(0,40))} ${ts} ${statusLabel}
${bodyHtml}
`; } el.innerHTML = html; } catch(e) { if (el) el.innerHTML = '
INTEL OFFLINE
'; } } function toggleIntelCard(id) { const card = document.getElementById('intel-card-' + id); if (!card) return; if (_intelActiveJobs.has(id)) _intelActiveJobs.delete(id); else _intelActiveJobs.add(id); card.classList.toggle('open'); } function startIntelPolling() { if (_intelPollTimer) return; _intelPollTimer = setInterval(() => { if (document.getElementById('tab-intel')?.classList.contains('active')) { loadIntel(); } }, 4000); } function stopIntelPolling() { if (_intelPollTimer) { clearInterval(_intelPollTimer); _intelPollTimer = null; } } function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function intelPrompt() { const input = document.getElementById('textInput'); if (input) { input.value = 'research '; input.focus(); } } // Called when arc_job is returned from chat response function onArcJobStarted(jobId, jobType) { const commsTypes = ['arc:gmail_triage', 'arc:send_email', 'arc:compose_email', 'arc:schedule_event', 'arc:meeting_prep']; if (commsTypes.includes(jobType)) { const commsBtn = document.getElementById('tab-btn-comms'); if (commsBtn) commsBtn.click(); startCommsPolling(); } else { _intelActiveJobs.add(jobId); const intelTab = document.querySelector('[onclick*="switchTab(\'intel\')"]'); if (intelTab) intelTab.click(); startIntelPolling(); } } // ── COMMS PROTOCOL — email triage HUD ──────────────────────────────────── let _commsPollTimer = null; let _commsFilter = 'priority'; let _commsOpenCards = new Set(); async function loadComms() { const el = document.getElementById('comms-list'); if (!el) return; try { const res = await api('arc?action=triage&limit=50&filter=' + _commsFilter); const items = Array.isArray(res) ? res : (res.items || []); if (!items.length) { el.innerHTML = '' + '
◈ NO TRIAGE DATA
Say "check my email" to activate
'; stopCommsPolling(); return; } const catOrder = {urgent:0, action:1, reply:2, meeting:3, info:4, promo:5, spam:6}; const catIcons = {urgent:'🔴', action:'⚡', reply:'◈', meeting:'📅', info:'ℹ', promo:'📢', spam:'🗑'}; let html = '
'; html += ''; html += ''; html += '
'; html += '
'; for (const [f, label] of [['priority','PRIORITY'],['urgent','URGENT'],['action','ACTION'],['all','ALL']]) { html += `
${label}
`; } html += '
'; for (const item of items) { const cat = item.category || 'info'; const icon = catIcons[cat] || '◈'; const prio = item.priority || 0; const isOpen = _commsOpenCards.has(item.id); const hasReply = item.draft_reply && item.draft_reply.trim().length > 5; html += `
${icon} ${cat.toUpperCase()} ${escHtml((item.subject||'(no subject)').substring(0,60))} ${prio}/10
FROM: ${escHtml((item.from_name||item.from_email||'').substring(0,50))}
${escHtml(item.summary||'')}
${hasReply ? `
DRAFT REPLY
${escHtml(item.draft_reply)}
` : ''}
${hasReply ? `` : ''} ${hasReply ? `` : ''}
`; } el.innerHTML = html; } catch(e) { if (el) el.innerHTML = '
COMMS OFFLINE
'; } } function toggleCommsCard(id) { const card = document.getElementById('comms-card-' + id); if (!card) return; if (_commsOpenCards.has(id)) _commsOpenCards.delete(id); else _commsOpenCards.add(id); card.classList.toggle('open'); } function commsSetFilter(f) { _commsFilter = f; loadComms(); } async function commsDismiss(id) { await api('arc?action=triage_action&id=' + id, 'POST', {action: 'dismissed'}).catch(() => {}); loadComms(); } async function commsCopyReply(id) { const draft = document.querySelector(`#comms-draft-${id}`); if (draft) { navigator.clipboard.writeText(draft.innerText).catch(() => {}); const btn = document.querySelector(`#comms-card-${id} [onclick*="commsCopyReply"]`); if (btn) { btn.textContent = 'COPIED!'; setTimeout(() => btn.textContent = 'COPY', 1500); } } } async function commsSendReply(id) { const btn = document.getElementById('comms-send-' + id); const draft = document.getElementById('comms-draft-' + id); if (!btn || !draft) return; btn.disabled = true; btn.textContent = '◈ SENDING…'; try { const res = await api('arc', 'POST', { action: 'job_create', type: 'send_email', payload: { triage_id: id, content: draft.innerText }, priority: 8, }); if (res.job_id) { btn.textContent = '◈ SENT ✓'; btn.style.color = '#00ff88'; setTimeout(() => loadComms(), 3000); loadCommsOutbox(); } else { btn.disabled = false; btn.textContent = '◈ SEND REPLY'; alert('Send failed: ' + (res.error || 'unknown error')); } } catch(e) { btn.disabled = false; btn.textContent = '◈ SEND REPLY'; } } function commsShowCompose() { const existing = document.getElementById('comms-compose-modal'); if (existing) existing.remove(); const modal = document.createElement('div'); modal.className = 'comms-compose-modal'; modal.id = 'comms-compose-modal'; modal.innerHTML = `
◈ COMPOSE MESSAGE
`; document.body.appendChild(modal); modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); }); } let _ccDraftedBody = ''; async function commsComposeDraft() { const to = document.getElementById('cc-to')?.value.trim(); const subject = document.getElementById('cc-subject')?.value.trim(); const instructions = document.getElementById('cc-instructions')?.value.trim(); const account = document.getElementById('cc-account')?.value; const status = document.getElementById('cc-status'); if (!to || !instructions) { if (status) status.textContent = 'Please fill in To and message description.'; return; } if (status) status.textContent = '◈ DRAFTING…'; try { const res = await api('arc', 'POST', { action: 'job_create', type: 'compose_email', payload: { recipient: to, subject, instructions, account, auto_send: false }, priority: 7, }); if (!res.job_id) throw new Error(res.error || 'No job'); // poll for result let attempts = 0; const poll = async () => { const job = await api('arc?action=job_get&id=' + res.job_id); if (job.status === 'done' && job.result?.drafted_body) { _ccDraftedBody = job.result.drafted_body; document.getElementById('cc-preview-body').textContent = _ccDraftedBody; document.getElementById('cc-preview').style.display = 'block'; document.getElementById('cc-send-btn').style.display = ''; if (status) status.textContent = '◈ DRAFT READY — Review and send'; } else if (job.status === 'failed') { if (status) status.textContent = '✗ Draft failed: ' + (job.error || 'unknown'); } else if (attempts++ < 20) { setTimeout(poll, 1500); } else { if (status) status.textContent = '◈ Job still running — check INTEL tab'; } }; setTimeout(poll, 1500); } catch(e) { if (status) status.textContent = '✗ Error: ' + e.message; } } async function commsComposeAndSend() { const to = document.getElementById('cc-to')?.value.trim(); const subject = document.getElementById('cc-subject')?.value.trim(); const account = document.getElementById('cc-account')?.value; const status = document.getElementById('cc-status'); const btn = document.getElementById('cc-send-btn'); if (!to || !_ccDraftedBody) return; if (btn) { btn.disabled = true; btn.textContent = '◈ SENDING…'; } if (status) status.textContent = '◈ TRANSMITTING…'; try { const res = await api('arc', 'POST', { action: 'job_create', type: 'send_email', payload: { to_email: to, subject, body: _ccDraftedBody, account }, priority: 9, }); if (res.job_id) { if (status) status.textContent = '◈ SENT ✓ (Job #' + res.job_id + ')'; setTimeout(() => { document.getElementById('comms-compose-modal')?.remove(); loadCommsOutbox(); }, 1500); } else { if (btn) { btn.disabled = false; btn.textContent = '◈ SEND NOW'; } if (status) status.textContent = '✗ Send failed: ' + (res.error || 'unknown'); } } catch(e) { if (btn) { btn.disabled = false; btn.textContent = '◈ SEND NOW'; } if (status) status.textContent = '✗ Error: ' + e.message; } } async function loadCommsOutbox() { const el = document.getElementById('comms-outbox'); if (!el) return; try { const data = await api('arc?action=comms_sent&limit=20'); const sent = Array.isArray(data) ? data : (data.sent || []); if (!sent.length) { el.innerHTML = '
No sent messages yet
'; return; } const statusColor = {sent:'#00ff88', failed:'#ff2244', queued:'#ffd700'}; let html = ''; for (const m of sent) { const ts = m.sent_at ? new Date(m.sent_at + 'Z').toLocaleString() : '—'; const sc = m.status || 'sent'; html += `
TO: ${escHtml((m.to_email||'').substring(0,40))}
${sc.toUpperCase()}
${escHtml((m.subject||'(no subject)').substring(0,60))}
${ts} · ${m.account||'gmail'}
`; } el.innerHTML = html; } catch(e) { el.innerHTML = '
OUTBOX OFFLINE
'; } } function commsTriageNow() { const input = document.getElementById('textInput'); if (input) { input.value = 'check my email'; input.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',keyCode:13,bubbles:true})); } } function startCommsPolling() { if (_commsPollTimer) return; _commsPollTimer = setInterval(() => { if (document.getElementById('tab-comms')?.classList.contains('active')) { loadComms(); loadCommsOutbox(); } }, 8000); } function stopCommsPolling() { if (_commsPollTimer) { clearInterval(_commsPollTimer); _commsPollTimer = null; } } // ── GUARDIAN MODE ───────────────────────────────────────────────────────────── let _guardianPollTimer = null; let _guardianChatTimer = null; let _guardianLastChat = ''; let _guardianUnread = 0; async function loadGuardian() { const el = document.getElementById('guardian-list'); if (!el) return; try { const [statusData, eventsData] = await Promise.all([ api('arc?action=guardian_status').catch(() => ({})), api('arc?action=guardian_events&limit=40').catch(() => []), ]); const events = Array.isArray(eventsData) ? eventsData : []; const status = statusData || {}; const counts = status.counts || {}; const unread = parseInt(counts.unread || 0); const critU = parseInt(counts.critical_unread || 0); _guardianUnread = unread; _updateGuardianBadge(unread, critU); const lastScan = status.last_scan ? new Date(status.last_scan + 'Z').toLocaleTimeString() : '—'; let html = `
◈ GUARDIAN MODE ${status.enabled ? '● ACTIVE' : '○ INACTIVE'} SCAN: ${lastScan} ${unread ? `` : ''}
`; if (!events.length) { html += '
◈ ALL CLEAR
Guardian is watching...
'; } else { for (const ev of events) { const sev = ev.severity || 'info'; const acked = ev.acknowledged; const ts = ev.created_at ? new Date(ev.created_at).toLocaleTimeString() : ''; const typeIco = {agent_offline:'⚠',agent_online:'✓',cpu_high:'⚡', mem_high:'⚡',disk_high:'💾',service_down:'✗', service_recovered:'✓',sitrep:'◈',anomaly:'◈'}[ev.event_type] || '◈'; html += `
${sev.toUpperCase()}
${typeIco} ${escHtml(ev.message||'')}
${ev.ai_analysis ? `
${escHtml(ev.ai_analysis.substring(0,200))}
` : ''}
${ts} ${!acked ? `` : ''}
`; } } el.innerHTML = html; startGuardianPolling(); } catch(e) { if (el) el.innerHTML = '
GUARDIAN OFFLINE
'; } } function _updateGuardianBadge(unread, critical) { const dot = document.getElementById('bb-guardian-dot'); const badge = document.getElementById('bb-guardian-badge'); const status = document.getElementById('bb-guardian-status'); if (!dot) return; dot.className = 'bb-dot'; if (critical > 0) { dot.classList.add('critical'); status.textContent = 'ALERT'; status.style.color = 'var(--red)'; } else if (unread > 0) { dot.classList.add('warning'); status.textContent = 'WARNING'; status.style.color = '#f5a623'; } else { dot.classList.add('all-clear'); status.textContent = 'CLEAR'; status.style.color = 'var(--green)'; } if (unread > 0) { badge.textContent = unread; badge.style.display = 'inline'; } else { badge.style.display = 'none'; } } async function guardianAck(id) { await api('arc?action=guardian_ack&id=' + id).catch(() => {}); const ev = document.getElementById('gev-' + id); if (ev) ev.classList.add('acked'); _guardianUnread = Math.max(0, _guardianUnread - 1); _updateGuardianBadge(_guardianUnread, 0); } async function guardianAckAll() { await api('arc?action=guardian_ack').catch(() => {}); loadGuardian(); } function guardianSitrep() { const input = document.getElementById('textInput'); if (input) { input.value = 'sitrep'; input.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',keyCode:13,bubbles:true})); } } function switchGuardianTab() { const btn = document.getElementById('tab-btn-guardian'); if (btn) btn.click(); } function startGuardianPolling() { if (_guardianPollTimer) return; _guardianPollTimer = setInterval(() => { if (document.getElementById('tab-guardian')?.classList.contains('active')) loadGuardian(); else _refreshGuardianBadge(); }, 30000); } async function _refreshGuardianBadge() { const s = await api('arc?action=guardian_status').catch(() => null); if (!s) return; const counts = s.counts || {}; _updateGuardianBadge(parseInt(counts.unread||0), parseInt(counts.critical_unread||0)); } // Proactive chat polling — checks for guardian-injected messages every 30s let _proactiveChatLastId = 0; async function _pollProactiveChat() { try { const rows = await api('arc?action=guardian_chat').catch(() => []); if (!Array.isArray(rows)) return; for (const row of rows) { if (row.id > _proactiveChatLastId) { _proactiveChatLastId = row.id; // Don't spam on first load — only show messages from last 5 min const age = Date.now() - new Date(row.created_at + 'Z').getTime(); if (age < 300000) { addMessage('jarvis', row.message); speak(row.message); } } } } catch(e) {} } // ── MISSION OPS HUD ─────────────────────────────────────────────────────────── let _missionsOpenCards = new Set(); async function loadMissionsHud() { const el = document.getElementById('missions-hud'); if (!el) return; try { const missions = await api('arc?action=missions'); const list = Array.isArray(missions) ? missions : []; let html = ''; if (!list.length) { html += '
◈ NO MISSIONS
Create workflows in Admin → Mission Ops
'; el.innerHTML = html; return; } const trigIcons = {manual:'🖐', schedule:'⏱', guardian_event:'🛡', email_keyword:'📧'}; for (const m of list) { const isOpen = _missionsOpenCards.has(m.id); const icon = trigIcons[m.trigger_type] || '◈'; const enabled = m.enabled; const lastRun = m.last_run_at ? new Date(m.last_run_at+'Z').toLocaleTimeString() : 'never'; html += `
${icon} ${escHtml(m.name)} ${m.trigger_type.replace('_',' ').toUpperCase()} ${m.run_count||0} runs
${m.description ? `
${escHtml(m.description)}
` : ''}
Last run: ${lastRun} · ${m.run_count||0} total runs
`; } el.innerHTML = html; } catch(e) { if (el) el.innerHTML = '
MISSIONS OFFLINE
'; } } function toggleMissionCard(id) { const card = document.getElementById('mission-card-' + id); if (!card) return; if (_missionsOpenCards.has(id)) _missionsOpenCards.delete(id); else _missionsOpenCards.add(id); card.classList.toggle('open'); } async function hudRunMission(id) { const btn = document.getElementById('mission-run-btn-' + id); const res = document.getElementById('mission-run-result-' + id); if (btn) { btn.disabled = true; btn.textContent = '◈ RUNNING…'; } if (res) res.textContent = ''; try { const data = await api('arc?action=mission_run&id=' + id, 'POST', {trigger_source: 'hud'}); const s = data.status || 'done'; const color = s === 'done' ? '#00ff88' : s === 'failed' ? '#ff2244' : '#ffd700'; if (res) res.style.color = color; if (res) res.textContent = `◈ ${s.toUpperCase()} — Run #${data.run_id||'?'} · ${data.steps||0} steps completed`; if (btn) { btn.disabled = false; btn.textContent = '▶ RUN NOW'; } setTimeout(loadMissionsHud, 2000); } catch(e) { if (btn) { btn.disabled = false; btn.textContent = '▶ RUN NOW'; } if (res) res.textContent = '✗ Run failed'; } } // ── DIRECTIVES HUD ──────────────────────────────────────────────────────────── let _dirOpenCards = new Set(); async function loadDirectivesHud() { const el = document.getElementById('directives-hud'); if (!el) return; try { const d = await api('directives/list?status=active'); const list = (d.directives || []); let html = ''; if (!list.length) { html += '
◈ NO ACTIVE DIRECTIVES
Create objectives in Admin → Directives
'; el.innerHTML = html; return; } const catColors = {work:'var(--cyan)',personal:'#a78bfa',health:'#00ff88',finance:'#ffd700',home:'var(--panel-border)',other:'var(--text-dim)'}; for (const dir of list) { const pct = Math.min(100, Math.round(dir.progress || 0)); const isOpen = _dirOpenCards.has(dir.id); const color = catColors[dir.category] || 'var(--cyan)'; const fillColor = pct >= 80 ? '#00ff88' : pct >= 40 ? '#ffd700' : '#ff6644'; const daysLeft = dir.target_date ? Math.ceil((new Date(dir.target_date) - new Date()) / 86400000) : null; const dueTxt = daysLeft !== null ? (daysLeft < 0 ? `OVERDUE ${Math.abs(daysLeft)}d` : `${daysLeft}d left`) : ''; const dueColor = daysLeft !== null && daysLeft < 0 ? '#ff2244' : daysLeft < 14 ? '#ffd700' : 'var(--text-dim)'; html += `
${dir.category.toUpperCase()} ${escHtml(dir.title)} ${pct}% ${dueTxt ? `${dueTxt}` : ''}
${dir.kr_count||0} KEY RESULTS · ${dir.link_count||0} LINKED ITEMS
`; } el.innerHTML = html; } catch(e) { if (el) el.innerHTML = '
DIRECTIVES OFFLINE
'; } } function toggleDirCard(id) { const card = document.getElementById('dir-card-' + id); if (!card) return; if (_dirOpenCards.has(id)) _dirOpenCards.delete(id); else _dirOpenCards.add(id); card.classList.toggle('open'); } async function hudDirectiveReview(id) { const res = await api('arc?action=job_create', 'POST', { type: 'directive_review', payload: {directive_id: id, provider: 'claude'}, priority: 6, }); if (res.job_id) { addMessage('jarvis', `◈ DIRECTIVE REVIEW initiated (Job #${res.job_id}). Analyzing objectives and key results now. Results will appear here shortly.`); speak(`Directive review underway. I'll brief you on your progress in a moment.`); } } // ── MEMORY CORE — bottom bar count ──────────────────────────────────────────── async function updateMemoryCount() { try { const stats = await api('memory?action=stats'); const el = document.getElementById('bb-memory-count'); const dot = document.getElementById('bb-memory-dot'); if (el && stats) { const total = stats.total || 0; el.textContent = total + ' FACTS'; if (dot) dot.style.background = total > 0 ? 'var(--cyan)' : 'rgba(0,212,255,0.3)'; } } catch(e) {} } // ── CLEARANCE PROTOCOL HUD ───────────────────────────────────────────────────── const _clrOpenCards = new Set(); async function updateClearanceBanner() { try { const pending = await api('arc?action=clearance_pending'); const list = Array.isArray(pending) ? pending : []; const count = list.length; const banner = document.getElementById('clearance-banner'); const badge = document.getElementById('clr-tab-badge'); const bcount = document.getElementById('clr-banner-count'); if (banner) { if (count > 0) { banner.classList.add('active'); if (bcount) bcount.textContent = count; } else { banner.classList.remove('active'); } } if (badge) { if (count > 0) { badge.style.display = 'inline'; badge.textContent = count; } else badge.style.display = 'none'; } } catch(e) {} } async function loadClearanceHud() { const el = document.getElementById('clearance-hud'); if (!el) return; try { const [pendingRes, rulesRes, historyRes] = await Promise.all([ api('arc?action=clearance_pending'), api('arc?action=clearance_rules'), api('arc?action=clearance_history&limit=20') ]); const pending = Array.isArray(pendingRes) ? pendingRes : []; const rules = Array.isArray(rulesRes) ? rulesRes : []; const history = Array.isArray(historyRes) ? historyRes : []; let html = ''; // Pending requests html += `
PENDING AUTHORIZATION (${pending.length})
`; if (!pending.length) { html += '
◈ NO PENDING CLEARANCE REQUESTS
'; } else { for (const cr of pending) { const isOpen = _clrOpenCards.has(cr.id); const pl = typeof cr.job_payload === 'string' ? JSON.parse(cr.job_payload || '{}') : (cr.job_payload || {}); const created = cr.created_at ? new Date(cr.created_at).toLocaleString() : ''; const expires = cr.expires_at ? new Date(cr.expires_at).toLocaleString() : ''; html += `
${escHtml(cr.job_type.toUpperCase().replace(/_/g,' '))} ${cr.risk_level.toUpperCase()} #${cr.id}
${escHtml(cr.description || 'No description')}
Requested: ${created}${expires ? ' · Expires: ' + expires : ''}
Payload: ${escHtml(JSON.stringify(pl))}
`; } } // Rules html += `
CLEARANCE RULES
`; if (!rules.length) { html += '
No rules configured
'; } else { html += '
'; for (const r of rules) { const enClass = r.enabled ? 'clr-rule-enabled' : 'clr-rule-disabled'; const enLabel = r.enabled ? 'ON' : 'OFF'; const reqLabel = r.require_approval ? 'REQUIRES APPROVAL' : 'AUTO-ALLOW'; const autoTxt = r.auto_approve_after_min ? ` · AUTO ${r.auto_approve_after_min}m` : ''; html += `
${r.job_type.replace(/_/g,' ').toUpperCase()} ${r.risk_level.toUpperCase()} ${reqLabel}${autoTxt}
`; } html += '
'; } // Recent history html += `
RECENT HISTORY
`; const recentDecided = history.filter(h => h.status !== 'pending').slice(0, 10); if (!recentDecided.length) { html += '
No history yet
'; } else { html += '
'; for (const h of recentDecided) { const ts = h.decided_at ? new Date(h.decided_at).toLocaleString() : ''; html += `
${h.job_type.replace(/_/g,' ').toUpperCase()} ${h.status.toUpperCase()} ${ts}
`; } html += '
'; } el.innerHTML = html; await updateClearanceBanner(); } catch(e) { if (el) el.innerHTML = '
CLEARANCE SYSTEM OFFLINE
'; } } function toggleClrCard(id) { const card = document.getElementById('clr-card-' + id); if (!card) return; if (_clrOpenCards.has(id)) _clrOpenCards.delete(id); else _clrOpenCards.add(id); card.classList.toggle('open'); } async function hudClearanceDecide(id, action) { const label = action === 'approve' ? 'AUTHORIZE' : 'DENY'; if (!confirm(`${label} clearance request #${id}?`)) return; const note = action === 'deny' ? (prompt('Reason for denial (optional):') || '') : ''; try { const res = await api(`arc?action=clearance_${action}&id=${id}`, 'POST', { decided_by: 'admin', note }); const msg = action === 'approve' ? `◈ Clearance #${id} authorized. Job dispatched.` : `◈ Clearance #${id} denied${note ? ': ' + note : ''}.`; addMessage('jarvis', msg); speak(action === 'approve' ? 'Clearance granted. Job dispatched.' : 'Request denied.'); await loadClearanceHud(); } catch(e) { addMessage('system', 'Clearance action failed.'); } } async function hudClearanceRuleToggle(id, newEnabled) { try { await api(`arc?action=clearance_rule_update&id=${id}`, 'POST', { enabled: newEnabled }); await loadClearanceHud(); } catch(e) {} } async function loadAgents() { const [listData, metricsData] = await Promise.all([ api('agent/list'), api('agent/status') ]); const agents = listData.agents || []; const metrics = metricsData.metrics || {}; // Fetch sparkline data (non-blocking) api('metrics').then(d => { _sparkData = d || {}; renderAgentsTab(agents, metrics); }).catch(() => {}); renderAgentsTab(agents, metrics); } async function addNetworkDevice() { const ip = prompt('IP address (e.g. 10.48.200.43):'); if (!ip) return; const name = prompt('Device name (e.g. Yealink Phone):'); if (!name) return; const type = prompt('Type (server, voip, nas, printer, device):', 'device') || 'device'; const r = await api('network/add', 'POST', {ip, alias: name, type}); if (r.error) { alert('Error: ' + r.error); return; } loadNetwork(); } async function deleteNetworkDevice(ip, evt) { evt.stopPropagation(); if (!confirm('Remove ' + ip + ' from the network list?')) return; const r = await api('network/delete', 'POST', {ip}); if (r.error) { alert('Error: ' + r.error); return; } loadNetwork(); } let _agentSparkData = {}; function sparkline(points, width=80, height=20, color='var(--cyan)') { if (!points || points.length < 2) return ''; const max = Math.max(...points, 1); const min = Math.min(...points); const range = max - min || 1; const step = width / (points.length - 1); const pts = points.map((v, i) => { const x = i * step; const y = height - ((v - min) / range) * (height - 2) - 1; return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); return ` `; } function renderAgentsTab(agents, metrics) { const el = document.getElementById('agents-list'); if (!el) return; if (!agents.length) { el.innerHTML = '
NO AGENTS REGISTERED
'; return; } el.innerHTML = agents.map(ag => { const m = metrics[ag.agent_id] || {}; const sys = m.system || {}; const alive = ag.status === 'online'; const cpu = sys.cpu_percent != null ? Math.round(sys.cpu_percent) : '--'; const mem = sys.memory ? Math.round(sys.memory.percent) : '--'; const memUsed = sys.memory ? Math.round(sys.memory.used_mb / 1024 * 10) / 10 + 'GB' : '--'; const memTot = sys.memory ? Math.round(sys.memory.total_mb / 1024 * 10) / 10 + 'GB' : '--'; const disks = sys.disk || []; const maxDisk = disks.length ? Math.max(...disks.map(d => parseInt(d.percent)||0)) : null; const uptime = sys.uptime ? sys.uptime.human : (alive ? 'ONLINE' : 'OFFLINE'); const since = ag.last_seen ? ag.last_seen.replace('T',' ').replace(/\.\d+Z$/,'') : '--'; const gauge = (val, unit='%', warn=80, crit=90) => { const v = typeof val === 'number' ? val : parseInt(val); if (isNaN(v)) return `--`; const col = v >= crit ? 'var(--red)' : v >= warn ? '#f5a623' : 'var(--green)'; return `
${v}${unit}
`; }; const svcs = (sys.services || []).filter(s => s.status !== 'inactive' || true) .map(s => `${s.service}: ${s.status}`) .join(''); const ctxKey = 'agent_' + ag.agent_id; _panelCtx[ctxKey] = {type:'agent', label: ag.hostname, agent_id: ag.agent_id, hostname: ag.hostname, status: ag.status, cpu, mem}; return `
${ag.hostname} ${ag.agent_type.toUpperCase()} · ${ag.ip_address} ${alive ? 'ONLINE' : 'OFFLINE'}
${alive ? `
CPU
${gauge(cpu)}
MEM ${memUsed}/${memTot}
${gauge(mem)}
DISK
${maxDisk != null ? gauge(maxDisk) : '--'}
CPU 2H
${sparkline((_agentSparkData[ag.agent_id]||[]).map(p=>p.cpu), 100, 18, 'rgba(0,212,255,0.7)')}
MEM 2H
${sparkline((_agentSparkData[ag.agent_id]||[]).map(p=>p.mem), 100, 18, 'rgba(0,255,136,0.7)')}
` : ''}
UP: ${uptime} · SEEN: ${since}
${svcs ? `
${svcs}
` : ''}
${alive ? `
` : ''}
`; }).join(''); } function openAgentModal() { const os = detectOS(); const title = document.getElementById('agentModalTitle'); const content = document.getElementById('agentModalContent'); const modal = document.getElementById('agentModal'); const regKey = 'f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518'; const baseUrl = 'https://jarvis.orbishosting.com/agent'; const jUrl = 'https://jarvis.orbishosting.com'; if (os === 'tablet') { title.textContent = '● JARVIS — TABLET / MOBILE'; content.innerHTML = '
✓ You\'re viewing JARVIS on a tablet or mobile device.
' + '
The JARVIS Agent runs on desktop and server platforms (Windows, macOS, Linux).

' + 'Tablets and phones can browse the full JARVIS dashboard but do not need an agent installed — all data comes from your other monitored machines.
'; } else if (_agentOnline) { title.textContent = '● AGENT CONNECTED'; content.innerHTML = '
✓ JARVIS Agent is active on this machine.
' + '
' + 'Host: ' + (_myAgent?.hostname||'—') + '
' + 'IP: ' + (_myAgent?.ip_address||'—') + '
' + 'Type: ' + (_myAgent?.agent_type||'—').toUpperCase() + '
' + 'Reporting: CPU · Memory · Disk · Services · Uptime
'; } else { const inst = { windows: { label:'Windows', cmd:'# Run PowerShell as Administrator:\nSet-ExecutionPolicy Bypass -Scope Process -Force\nInvoke-WebRequest -Uri "'+baseUrl+'/install-windows.ps1" -OutFile "$env:TEMP\\install.ps1"\n& "$env:TEMP\\install.ps1" -JarvisUrl '+jUrl+' -Key '+regKey, dl: baseUrl+'/install-windows.ps1', note:'Run PowerShell as Administrator. Installs as a Windows Task Scheduler service.' }, mac: { label:'macOS', cmd:'bash <(curl -sSL '+baseUrl+'/install-mac.sh) \\\n --jarvis-url '+jUrl+' \\\n --key '+regKey, dl: baseUrl+'/install-mac.sh', note:'Run in Terminal. Installs as a launchd background service.' }, linux: { label:'Linux', cmd:'curl -sSL '+baseUrl+'/install.sh | sudo bash -s -- \\\n --jarvis-url '+jUrl+' \\\n --key '+regKey, dl: baseUrl+'/install.sh', note:'Run in terminal. Installs as a systemd service.' }, unknown: { label:'Your System', cmd:'# Browse installers:\nhttps://jarvis.orbishosting.com/agent/', dl: 'https://jarvis.orbishosting.com/agent/', note:'Choose your platform installer from the JARVIS agent directory.' } }; const i = inst[os] || inst.unknown; const osBadge = {windows:'🪟 WINDOWS', mac:'🍎 MACOS', linux:'🐧 LINUX', unknown:'❓ UNKNOWN'}[os] || os.toUpperCase(); title.textContent = '● INSTALL AGENT · ' + (inst[os] ? inst[os].label.toUpperCase() : 'YOUR SYSTEM'); content.innerHTML = '
DETECTED: ' + osBadge + '
' + '
'+i.note+'
' + '
'+i.cmd+'
' + '↓ DOWNLOAD INSTALLER' + '
After install, the AGENT indicator turns green within 30 seconds.
'; } modal.classList.add('open'); } document.addEventListener('click', function(e) { if (e.target === document.getElementById('agentModal')) document.getElementById('agentModal').classList.remove('open'); }); // ── SITES MANAGER ──────────────────────────────────────────────────── let sitesData = {}; function openSitesModal() { document.getElementById('sitesModal').style.display = 'flex'; loadSites(); } function closeSitesModal() { document.getElementById('sitesModal').style.display = 'none'; } // Close on backdrop click document.getElementById('sitesModal').addEventListener('click', function(e) { if (e.target === this) closeSitesModal(); }); async function loadSites() { document.getElementById('sites-grid').innerHTML = '
LOADING SITE SETTINGS...
'; const res = await api('sites'); if (!res.success) { document.getElementById('sites-grid').innerHTML = '
FAILED TO LOAD SETTINGS
'; return; } sitesData = res.sites; // Pre-fill global key from first site const firstKey = Object.values(res.sites)[0]?.api_key || ''; document.getElementById('global-api-key').value = firstKey; renderSiteCards(); } function renderSiteCards() { const grid = document.getElementById('sites-grid'); let html = ''; for (const [id, s] of Object.entries(sitesData)) { html += `
${s.name.toUpperCase()}
${s.url}
FROM EMAIL
FROM NAME
ADMIN NOTIFICATION EMAIL
`; } grid.innerHTML = html; } async function pushApiKey() { const key = document.getElementById('global-api-key').value.trim(); const status = document.getElementById('push-status'); if (!key) { status.style.color='#f44'; status.textContent='✗ API KEY REQUIRED'; return; } status.style.color='var(--text-dim)'; status.textContent='PUSHING TO ALL SITES...'; const res = await api('sites', 'POST', {action:'push_key', api_key:key}); if (res.success) { const ok = Object.values(res.results).filter(Boolean).length; const total = Object.keys(res.results).length; status.style.color = ok === total ? 'var(--cyan)' : '#fa0'; status.textContent = `✓ PUSHED TO ${ok}/${total} SITES`; for (const id of Object.keys(sitesData)) sitesData[id].api_key = key; } else { status.style.color='#f44'; status.textContent='✗ ' + (res.error || 'FAILED'); } } async function saveSite(id) { const status = document.getElementById(id + '-status'); status.style.color='var(--text-dim)'; status.textContent='SAVING...'; const res = await api('sites', 'POST', { action: 'save', site: id, from_email: document.getElementById(id+'-from_email').value.trim(), from_name: document.getElementById(id+'-from_name').value.trim(), admin_email: document.getElementById(id+'-admin_email').value.trim(), }); if (res.success) { status.style.color='var(--cyan)'; status.textContent='✓ SAVED'; setTimeout(() => { status.textContent=''; }, 3000); } else { status.style.color='#f44'; status.textContent='✗ ' + (res.error || 'FAILED'); } } // ── VISION PROTOCOL — screenshot lightbox ──────────────────────────────────── function openVisionLightbox(title) { const lb = document.getElementById('vision-lightbox'); document.getElementById('vision-lb-title').textContent = title || '◈ VISION PROTOCOL'; document.getElementById('vision-lb-img').style.display = 'none'; document.getElementById('vision-lb-img').src = ''; document.getElementById('vision-lb-analysis').textContent = ''; document.getElementById('vision-lb-spinner').style.display = 'block'; lb.classList.add('open'); } function closeVisionLightbox() { document.getElementById('vision-lightbox').classList.remove('open'); } async function agentScreenshot(hostname) { openVisionLightbox('◈ VISION PROTOCOL — ' + hostname.toUpperCase()); const arcRes = await api('arc?action=job_create', 'POST', { type: 'screenshot', payload: {agent: hostname, analyze: true}, priority: 8, }).catch(() => null); if (!arcRes || !arcRes.job_id) { document.getElementById('vision-lb-spinner').style.display = 'none'; document.getElementById('vision-lb-analysis').textContent = 'Failed to submit screenshot job — Arc Reactor may be offline.'; return; } // Poll for result const jobId = arcRes.job_id; let tries = 0; const poll = async () => { tries++; const job = await api('arc?action=job_get&id=' + jobId).catch(() => null); if (job && job.status === 'done') { const r = job.result || {}; document.getElementById('vision-lb-spinner').style.display = 'none'; if (r.has_image && r.screenshot_id) { // Fetch full screenshot with image const full = await api('arc?action=screenshot_get&id=' + r.screenshot_id).catch(() => null); if (full && full.image_b64) { const img = document.getElementById('vision-lb-img'); img.src = 'data:image/png;base64,' + full.image_b64; img.style.display = 'block'; } } document.getElementById('vision-lb-analysis').textContent = r.analysis || (r.has_image ? 'Screenshot captured — no analysis available.' : JSON.stringify(r.snapshot || r, null, 2)); } else if (job && job.status === 'failed') { document.getElementById('vision-lb-spinner').style.display = 'none'; document.getElementById('vision-lb-analysis').textContent = 'Screenshot failed: ' + (job.error || 'Unknown error'); } else if (tries < 30) { setTimeout(poll, 2000); } else { document.getElementById('vision-lb-spinner').style.display = 'none'; document.getElementById('vision-lb-analysis').textContent = 'Timed out waiting for screenshot.'; } }; setTimeout(poll, 2000); } async function agentSysinfo(hostname) { openVisionLightbox('⚡ FIELD SYSINFO — ' + hostname.toUpperCase()); const arcRes = await api('arc?action=job_create', 'POST', { type: 'sysinfo', payload: {agent: hostname, analyze: true}, priority: 7, }).catch(() => null); if (!arcRes || !arcRes.job_id) { document.getElementById('vision-lb-spinner').style.display = 'none'; document.getElementById('vision-lb-analysis').textContent = 'Failed to submit sysinfo job.'; return; } const jobId = arcRes.job_id; let tries = 0; const poll = async () => { tries++; const job = await api('arc?action=job_get&id=' + jobId).catch(() => null); if (job && job.status === 'done') { const r = job.result || {}; document.getElementById('vision-lb-spinner').style.display = 'none'; const snap = r.snapshot || {}; const snapText = Object.entries(snap) .filter(([k]) => !['success','screenshot_available','snapshot_type'].includes(k)) .map(([k,v]) => `${k.toUpperCase().replace(/_/g,' ')}: ${Array.isArray(v) ? v.join('\n ') : v}`) .join('\n'); document.getElementById('vision-lb-analysis').textContent = (r.analysis ? r.analysis + '\n\n─────────────────────\n\n' : '') + (snapText || JSON.stringify(r, null, 2)); } else if (job && job.status === 'failed') { document.getElementById('vision-lb-spinner').style.display = 'none'; document.getElementById('vision-lb-analysis').textContent = 'Sysinfo failed: ' + (job.error || 'Unknown error'); } else if (tries < 20) { setTimeout(poll, 2000); } else { document.getElementById('vision-lb-spinner').style.display = 'none'; document.getElementById('vision-lb-analysis').textContent = 'Timed out.'; } }; setTimeout(poll, 2000); } document.addEventListener('keydown', e => { if (e.key === 'Escape') closeVisionLightbox(); }); // ── CHAT HISTORY SEARCH ─────────────────────────────────────────────────────── function openSearchModal() { document.getElementById('searchModal').style.display = 'flex'; document.getElementById('searchInput').focus(); } function closeSearchModal() { document.getElementById('searchModal').style.display = 'none'; document.getElementById('searchResults').innerHTML = '
Type to search your JARVIS conversations
'; document.getElementById('searchInput').value = ''; } async function runSearch() { const q = document.getElementById('searchInput').value.trim(); if (!q) return; const el = document.getElementById('searchResults'); el.innerHTML = '
Searching...
'; try { const d = await api('history?q=' + encodeURIComponent(q)); if (!d.results || !d.results.length) { el.innerHTML = '
No results for "' + q + '"
'; return; } el.innerHTML = d.results.map(r => { const role = r.role === 'user' ? '👤' : '🤖'; const ts = new Date(r.created_at).toLocaleString('en-US', {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}); const snippet = r.content.length > 200 ? r.content.slice(0,197) + '…' : r.content; return `
${role} ${r.role.toUpperCase()} ${ts}
${snippet.replace(/
`; }).join(''); } catch(e) { el.innerHTML = '
Search failed
'; } } document.getElementById('searchModal')?.addEventListener('click', e => { if (e.target === document.getElementById('searchModal')) closeSearchModal(); }); // ── PROACTIVE SUGGESTIONS ──────────────────────────────────────────────────── const _shownSuggestions = new Set(); async function checkSuggestions() { const d = await api('suggestions').catch(() => null); if (!d || !d.suggestions || !d.suggestions.length) return; for (const s of d.suggestions) { const key = s.intent + ':' + d.hour + ':' + d.dow; if (_shownSuggestions.has(key)) continue; _shownSuggestions.add(key); // Show as a soft suggestion chip in chat const log = document.getElementById('chatLog'); const chip = document.createElement('div'); chip.style.cssText = 'display:flex;justify-content:flex-end;margin:4px 0'; chip.innerHTML = ``; log.appendChild(chip); log.scrollTop = log.scrollHeight; break; // show max one suggestion at a time } } function sendSuggestion(intent, btn) { btn.closest('div').remove(); const prompts = { 'network_scan': 'run a network scan', 'jellyfin_now_playing': 'what is playing on Jellyfin', 'ha_scene': 'what scenes are available', 'planner:briefing': 'daily briefing', 'vm_suggestions': 'VM resource suggestions', 'focus_mode': 'focus mode', }; const msg = prompts[intent] || intent.replace(/_/g,' '); document.getElementById('textInput').value = msg; sendMessage(); } // ── MOBILE PANEL SWITCHER ───────────────────────────────────────────────────── function mobSwitch(which) { if (window.innerWidth > 900) return; const panels = {left:'leftPanel', center:'centerPanel', right:'rightPanel'}; Object.entries(panels).forEach(([k, id]) => { document.getElementById(id)?.classList.toggle('mob-active', k === which); }); document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active')); document.getElementById('mob-btn-' + which)?.classList.add('active'); if (which === 'right') loadNews(); } function initMobile() { if (window.innerWidth > 900) return; ['leftPanel','centerPanel','rightPanel'].forEach(id => document.getElementById(id)?.classList.remove('mob-active')); document.getElementById('leftPanel')?.classList.add('mob-active'); document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active')); document.getElementById('mob-btn-left')?.classList.add('active'); } window.addEventListener('resize', initMobile); // ── COMMAND PALETTE (Ctrl+K) ────────────────────────────────────────────── const _PALETTE_COMMANDS = [ { label: 'Run a network scan', q: 'run a network scan', group: 'Network' }, { label: 'Show online devices', q: 'who is online on the network', group: 'Network' }, { label: 'Proxmox status', q: 'proxmox status', group: 'Network' }, { label: 'Check agent status', q: 'check all agents', group: 'Agents' }, { label: 'Restart JARVIS agent', q: 'restart jarvis agent', group: 'Agents' }, { label: 'Check VM resources', q: 'VM resource suggestions', group: 'Agents' }, { label: 'Daily briefing', q: 'daily briefing', group: 'Planner' }, { label: 'My tasks today', q: 'my tasks today', group: 'Planner' }, { label: 'My calendar', q: 'my calendar', group: 'Planner' }, { label: 'What's playing on Jellyfin', q: 'what is playing on Jellyfin', group: 'Media' }, { label: 'Pause Jellyfin', q: 'pause Jellyfin', group: 'Media' }, { label: 'Next track on Jellyfin', q: 'next track on Jellyfin', group: 'Media' }, { label: 'Stop Jellyfin', q: 'stop Jellyfin', group: 'Media' }, { label: 'List HA scenes', q: 'show home assistant scenes', group: 'Smart Home'}, { label: 'Activate scene…', q: 'activate scene ', group: 'Smart Home'}, { label: 'Focus mode', q: 'focus mode', group: 'UI' }, { label: 'Show all panels', q: 'show all panels', group: 'UI' }, { label: 'Check alerts', q: 'check alerts', group: 'System' }, { label: 'Site health', q: 'site health', group: 'System' }, { label: 'System status', q: 'system status', group: 'System' }, { label: 'Check inbox', q: 'check inbox', group: 'Comms' }, { label: 'Search history…', q: '', group: 'Chat', search: true }, ]; let _paletteOpen = false; function openPalette() { if (_paletteOpen) return; _paletteOpen = true; const ov = document.getElementById('cmdPalette'); if (!ov) return; ov.style.display = 'flex'; const inp = document.getElementById('cmdPaletteInput'); inp.value = ''; renderPaletteItems(''); requestAnimationFrame(() => { ov.classList.add('open'); inp.focus(); }); } function closePalette() { if (!_paletteOpen) return; _paletteOpen = false; const ov = document.getElementById('cmdPalette'); if (!ov) return; ov.classList.remove('open'); setTimeout(() => { ov.style.display = 'none'; }, 180); } function renderPaletteItems(q) { const list = document.getElementById('cmdPaletteList'); if (!list) return; const low = q.toLowerCase().trim(); const filtered = low ? _PALETTE_COMMANDS.filter(c => c.label.toLowerCase().includes(low) || c.group.toLowerCase().includes(low)) : _PALETTE_COMMANDS; let currentGroup = null; list.innerHTML = ''; filtered.forEach((cmd, i) => { if (cmd.group !== currentGroup) { currentGroup = cmd.group; const g = document.createElement('div'); g.className = 'cp-group'; g.textContent = cmd.group; list.appendChild(g); } const row = document.createElement('div'); row.className = 'cp-item' + (i === 0 ? ' cp-active' : ''); row.dataset.q = cmd.q; row.dataset.search = cmd.search ? '1' : ''; const lbl = cmd.label.replace(new RegExp(low.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi'), m => `${m}`); row.innerHTML = `${lbl}`; row.addEventListener('click', () => firePaletteItem(row)); list.appendChild(row); }); } function movePaletteSelection(dir) { const items = Array.from(document.querySelectorAll('#cmdPaletteList .cp-item')); if (!items.length) return; const cur = items.findIndex(el => el.classList.contains('cp-active')); const next = (cur + dir + items.length) % items.length; items.forEach(el => el.classList.remove('cp-active')); items[next].classList.add('cp-active'); items[next].scrollIntoView({ block: 'nearest' }); } function firePaletteItem(el) { if (!el) { const active = document.querySelector('#cmdPaletteList .cp-active'); if (!active) return; el = active; } const q = el.dataset.q; const isSearch = el.dataset.search === '1'; closePalette(); if (isSearch) { if (typeof openSearchModal === 'function') openSearchModal(); return; } if (q) { document.getElementById('textInput').value = q; sendMessage(); } } // Keyboard events document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); _paletteOpen ? closePalette() : openPalette(); return; } if (!_paletteOpen) return; if (e.key === 'Escape') { e.preventDefault(); closePalette(); } if (e.key === 'ArrowDown') { e.preventDefault(); movePaletteSelection(1); } if (e.key === 'ArrowUp') { e.preventDefault(); movePaletteSelection(-1); } if (e.key === 'Enter') { e.preventDefault(); firePaletteItem(null); } }); // Filter on type document.getElementById('cmdPaletteInput')?.addEventListener('input', e => { renderPaletteItems(e.target.value); }); // Close on backdrop click document.getElementById('cmdPalette')?.addEventListener('click', e => { if (e.target.id === 'cmdPalette') closePalette(); });