// ── 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));
}
}