// ── palette.js — command palette, error badge, global search import { escapeHTML } from './utils.js'; import { api, showToast } from './api.js'; import { cycleTheme } from './theme.js'; import { agentsState, isAgentOnline } from './state.js'; import { navigate } from './router.js'; import { selectAgent } from './pages/agents.js'; // ── Error badge ────────────────────────────────────────── let _unseenErrors = 0; export function incrementErrorBadge() { if (window.location.pathname === '/') return; // On dashboard, don't badge _unseenErrors++; const badge = document.getElementById('nav-error-badge'); if (badge) { badge.textContent = _unseenErrors > 99 ? '99+' : String(_unseenErrors); badge.classList.add('visible'); } } export function clearErrorBadge() { _unseenErrors = 0; const badge = document.getElementById('nav-error-badge'); if (badge) { badge.classList.remove('visible'); badge.textContent = ''; } } // ── Command palette ────────────────────────────────────── let paletteOpen = false; let paletteSelectedIndex = 0; export function isCommandPaletteOpen() { return paletteOpen; } export function getCommandPaletteItems(query) { const items = [ { label: 'Dashboard', path: '/', icon: '◉', shortcut: 'g d' }, { label: 'Sessions', path: '/sessions', icon: '▶', shortcut: 'g s' }, { label: 'Agents', path: '/agents', icon: '◎', shortcut: 'g a' }, { label: 'Infrastructure', path: '/infrastructure', icon: '⚡', shortcut: 'g i' }, { label: 'Settings', path: '/settings', icon: '⚙', shortcut: 'g p' }, { label: 'Usage', path: '/usage', icon: '◈', shortcut: 'g u' }, { label: 'Toggle Theme', action: 'theme', icon: '◐' }, ]; // Add agent items dynamically if (agentsState && agentsState.agents) { for (const [key, agent] of Object.entries(agentsState.agents)) { items.push({ label: 'Agent: ' + (agent.name || key), path: '/agents', action: 'select-agent', agentKey: key, icon: isAgentOnline(agent) ? '●' : '○', }); } } if (!query) return items; const q = query.toLowerCase(); return items.filter(item => item.label.toLowerCase().includes(q)); } export function openCommandPalette() { if (paletteOpen) return; paletteOpen = true; paletteSelectedIndex = 0; const backdrop = document.createElement('div'); backdrop.className = 'cmd-palette-backdrop'; backdrop.id = 'cmd-palette-backdrop'; backdrop.innerHTML = `
`; document.body.appendChild(backdrop); const input = document.getElementById('cmd-palette-input'); input.focus(); renderPaletteItems(''); input.addEventListener('input', () => { paletteSelectedIndex = 0; renderPaletteItems(input.value); }); input.addEventListener('keydown', (e) => { const items = document.querySelectorAll('.cmd-palette-item'); if (e.key === 'ArrowDown') { e.preventDefault(); paletteSelectedIndex = Math.min(paletteSelectedIndex + 1, items.length - 1); updatePaletteSelection(); } else if (e.key === 'ArrowUp') { e.preventDefault(); paletteSelectedIndex = Math.max(paletteSelectedIndex - 1, 0); updatePaletteSelection(); } else if (e.key === 'Enter') { e.preventDefault(); const selected = items[paletteSelectedIndex]; if (selected) selected.click(); } else if (e.key === 'Escape') { closeCommandPalette(); } }); backdrop.addEventListener('click', (e) => { if (e.target === backdrop) closeCommandPalette(); }); } export function closeCommandPalette() { paletteOpen = false; const backdrop = document.getElementById('cmd-palette-backdrop'); if (backdrop) backdrop.remove(); } function renderPaletteItems(query) { const container = document.getElementById('cmd-palette-results'); if (!container) return; const items = getCommandPaletteItems(query); // If query looks like an ID (4+ hex chars), add a search option if (query.length >= 4) { items.unshift({ label: 'Search: ' + query, action: 'search', query, icon: '🔍' }); } container.innerHTML = items.map((item, i) => `
${item.icon}
${escapeHTML(item.label)} ${item.shortcut ? `${item.shortcut}` : ''}
`).join(''); container.querySelectorAll('.cmd-palette-item').forEach((el, i) => { el.addEventListener('click', () => executePaletteItem(items[i])); el.addEventListener('mouseenter', () => { paletteSelectedIndex = i; updatePaletteSelection(); }); }); } function updatePaletteSelection() { document.querySelectorAll('.cmd-palette-item').forEach((el, i) => { el.classList.toggle('selected', i === paletteSelectedIndex); if (i === paletteSelectedIndex) el.scrollIntoView({ block: 'nearest' }); }); } function executePaletteItem(item) { closeCommandPalette(); if (item.action === 'theme') { cycleTheme(); } else if (item.action === 'search') { handleGlobalSearch(item.query); } else if (item.action === 'select-agent') { navigate('/agents'); setTimeout(() => selectAgent(item.agentKey, 'live'), 100); } else if (item.path) { navigate(item.path); } } // ── Global search ──────────────────────────────────────── export async function handleGlobalSearch(query) { query = query.trim(); if (query.length < 4) { showToast('Enter at least 4 characters', 'info'); return; } // Hex ID pattern — try as session or run if (/^[a-f0-9-]{4,}$/i.test(query)) { try { const sessionData = await api('/v1/sessions/' + query).catch(() => null); if (sessionData && sessionData.session) { navigate('/sessions/' + query); return; } const runData = await api('/v1/runs/' + query).catch(() => null); if (runData && runData.run) { navigate('/runs/' + query); return; } showToast('ID not found', 'error'); } catch (e) { showToast('Search failed: ' + e.message, 'error'); } } else { // Non-hex: treat as framework/host search navigate('/sessions?framework=' + encodeURIComponent(query)); } }