// ── app.js — entry point ────────────────────────────────── // Thin bootstrap: imports modules, wires DOM events, fires route(). // Navigation and copy-to-clipboard are wired via delegated click listeners // below — no inline onclick= attributes, no window.* globals. import { navigate, route } from './modules/router.js'; import { connectWS, subscribeWS, updateWSIndicator } from './modules/ws.js'; import { applyTheme, getTheme, updateToggleBtn, cycleTheme } from './modules/theme.js'; import { copyToClipboard } from './modules/api.js'; import { getEnvelopeType } from './modules/utils.js'; import { incrementErrorBadge, openCommandPalette, closeCommandPalette, isCommandPaletteOpen, handleGlobalSearch, } from './modules/palette.js'; // ── Delegated click handlers ────────────────────────────── // 1) Same-origin clicks → SPA navigation (no full page reload). // 2) .copy-btn[data-copy] → copyToClipboard. function isInternalLinkClick(e, a) { if (e.defaultPrevented) return false; if (e.button !== 0) return false; if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return false; if (a.target && a.target !== '' && a.target !== '_self') return false; if (a.hasAttribute('download')) return false; const href = a.getAttribute('href'); if (!href) return false; if (href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return false; let url; try { url = new URL(href, window.location.href); } catch { return false; } if (url.origin !== window.location.origin) return false; return url; } document.addEventListener('click', (e) => { // Copy buttons first — stop the click from reaching a surrounding row link. const copyBtn = e.target.closest('.copy-btn[data-copy]'); if (copyBtn) { e.preventDefault(); e.stopPropagation(); copyToClipboard(copyBtn.getAttribute('data-copy'), copyBtn); return; } const a = e.target.closest('a'); if (!a) return; const url = isInternalLinkClick(e, a); if (!url) return; e.preventDefault(); navigate(url.pathname + url.search + url.hash); }); // ── Bootstrap ───────────────────────────────────────────── applyTheme(getTheme()); document.addEventListener('DOMContentLoaded', function() { updateToggleBtn(getTheme()); const btn = document.getElementById('theme-toggle'); if (btn) btn.addEventListener('click', cycleTheme); // Global Search const searchInput = document.getElementById('global-search'); if (searchInput) { searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { const val = searchInput.value.trim(); if (val) handleGlobalSearch(val); } }); } // Cmd+K hint button — show platform-appropriate label const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgentData?.platform || ''); const cmdKBtn = document.getElementById('cmd-k-hint'); if (cmdKBtn) { cmdKBtn.innerHTML = `${isMac ? '⌘K' : 'Ctrl+K'}`; cmdKBtn.addEventListener('click', openCommandPalette); } // Global error tracking — persists across page navigations subscribeWS(function globalErrorTracker(msg) { if (msg.type !== 'message') return; if (getEnvelopeType(msg.data) === 'error') { incrementErrorBadge(); } }); // Keyboard shortcuts let _pendingGoto = false; document.addEventListener('keydown', (e) => { // Ignore when typing in inputs if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) { if (e.key === 'Escape') document.activeElement.blur(); return; } // Cmd+K or Ctrl+K — command palette if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); if (isCommandPaletteOpen()) closeCommandPalette(); else openCommandPalette(); return; } // '/' to focus search if (e.key === '/' && !isCommandPaletteOpen()) { e.preventDefault(); const si = document.getElementById('global-search'); if (si) si.focus(); return; } // Escape closes palette if (e.key === 'Escape' && isCommandPaletteOpen()) { closeCommandPalette(); return; } // 'g' prefix for goto shortcuts if (e.key === 'g' && !_pendingGoto) { _pendingGoto = true; setTimeout(() => { _pendingGoto = false; }, 800); return; } if (_pendingGoto) { _pendingGoto = false; if (e.key === 'd') navigate('/'); else if (e.key === 's') navigate('/sessions'); else if (e.key === 'a') navigate('/agents'); else if (e.key === 'i') navigate('/infrastructure'); else if (e.key === 'p') navigate('/settings'); else if (e.key === 'u') navigate('/usage'); return; } }); connectWS(); updateWSIndicator(); route(); });