/** * Impeccable DevTools Extension - Panel * * Displays findings, provides controls for scanning and overlay toggling, * and allows clicking findings to inspect elements. */ // Match the DevTools theme (light or dark) if (chrome.devtools.panels.themeName === 'dark') { document.documentElement.classList.add('theme-dark'); } const tabId = chrome.devtools.inspectedWindow.tabId; // Auto-reconnecting port. Service workers in MV3 can be terminated after ~30s of // inactivity (especially when the browser window is unfocused). When they restart, // the existing port becomes invalid. We recreate it lazily on the next use. let port = null; function getPort() { if (port) return port; port = chrome.runtime.connect({ name: `impeccable-panel-${tabId}` }); port.onMessage.addListener(handlePortMessage); port.onDisconnect.addListener(() => { port = null; }); return port; } function postToPort(msg) { try { getPort().postMessage(msg); } catch { // Port died mid-call. Drop it and try once more with a fresh port. port = null; try { getPort().postMessage(msg); } catch { /* give up silently */ } } } const badge = document.getElementById('badge'); const container = document.getElementById('findings-container'); const emptyState = document.getElementById('empty-state'); const btnRescan = document.getElementById('btn-rescan'); const btnToggle = document.getElementById('btn-toggle'); const btnCopyAll = document.getElementById('btn-copy-all'); const settingsContainer = document.getElementById('settings-container'); const settingsList = document.getElementById('settings-list'); const btnSettings = document.getElementById('btn-settings'); let overlaysVisible = true; let allAntipatterns = []; let disabledRules = []; let currentFindings = []; // Load antipatterns list and disabled rules async function initSettings() { try { const resp = await fetch(chrome.runtime.getURL('detector/antipatterns.json')); allAntipatterns = await resp.json(); } catch { allAntipatterns = []; } const stored = await chrome.storage.sync.get({ disabledRules: [], lineLengthMode: 'strict', spotlightBlur: true, autoScan: 'panel', }); disabledRules = stored.disabledRules; renderSettings(); initLineLengthControl(stored.lineLengthMode); initSpotlightBlurToggle(stored.spotlightBlur); initAutoScanControl(stored.autoScan); } function initAutoScanControl(currentMode) { const group = document.getElementById('auto-scan-mode'); if (!group) return; for (const btn of group.querySelectorAll('button')) { btn.classList.toggle('active', btn.dataset.value === currentMode); btn.addEventListener('click', async () => { const mode = btn.dataset.value; for (const b of group.querySelectorAll('button')) { b.classList.toggle('active', b === btn); } await chrome.storage.sync.set({ autoScan: mode }); }); } } function initLineLengthControl(currentMode) { const group = document.getElementById('line-length-mode'); if (!group) return; for (const btn of group.querySelectorAll('button')) { btn.classList.toggle('active', btn.dataset.value === currentMode); btn.addEventListener('click', async () => { const mode = btn.dataset.value; for (const b of group.querySelectorAll('button')) { b.classList.toggle('active', b === btn); } await chrome.storage.sync.set({ lineLengthMode: mode }); chrome.runtime.sendMessage({ action: 'disabled-rules-changed' }); }); } } function initSpotlightBlurToggle(currentValue) { const cb = document.getElementById('spotlight-blur-toggle'); if (!cb) return; cb.checked = currentValue; cb.addEventListener('change', async () => { await chrome.storage.sync.set({ spotlightBlur: cb.checked }); chrome.runtime.sendMessage({ action: 'disabled-rules-changed' }); }); } function renderSettings() { settingsList.innerHTML = ''; const categories = { slop: { label: 'AI tells', items: [] }, quality: { label: 'Quality', items: [] }, }; for (const ap of allAntipatterns) { const cat = ap.category || 'quality'; (categories[cat] || categories.quality).items.push(ap); } for (const [, group] of Object.entries(categories)) { if (!group.items.length) continue; const header = document.createElement('div'); header.className = 'settings-header'; header.textContent = group.label; settingsList.appendChild(header); const grid = document.createElement('div'); grid.className = 'settings-grid'; for (const ap of group.items) { const label = document.createElement('label'); label.className = 'setting-rule'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = !disabledRules.includes(ap.id); checkbox.addEventListener('change', () => toggleRule(ap.id, checkbox.checked)); const text = document.createElement('span'); text.textContent = ap.name; label.appendChild(checkbox); label.appendChild(text); grid.appendChild(label); } settingsList.appendChild(grid); } } async function toggleRule(ruleId, enabled) { if (enabled) { disabledRules = disabledRules.filter(id => id !== ruleId); } else { if (!disabledRules.includes(ruleId)) disabledRules.push(ruleId); } await chrome.storage.sync.set({ disabledRules }); chrome.runtime.sendMessage({ action: 'disabled-rules-changed' }); } // Listen for messages from the service worker (called by getPort() on each new connection) function handlePortMessage(msg) { if (msg.action === 'page-pointer-active') { // Cursor is active on the page → user has left the panel setHoveredItem(null); return; } if (msg.action === 'findings' || msg.action === 'state') { renderFindings(msg.findings || []); if (msg.overlaysVisible !== undefined) { overlaysVisible = msg.overlaysVisible; updateToggleButton(); } } if (msg.action === 'overlays-toggled') { overlaysVisible = msg.visible; updateToggleButton(); } if (msg.action === 'navigated') { showScanning(); } } // Initial connection getPort(); // Heartbeat to keep the MV3 service worker alive while the panel is open. // SWs can be terminated after ~30s of inactivity, especially when the browser is unfocused. setInterval(() => postToPort({ action: 'ping' }), 20000); // Controls btnRescan.addEventListener('click', () => { showScanning(); postToPort({ action: 'scan' }); }); btnToggle.addEventListener('click', () => { postToPort({ action: 'toggle-overlays' }); }); btnSettings.addEventListener('click', () => { const isVisible = settingsContainer.style.display !== 'none'; settingsContainer.style.display = isVisible ? 'none' : ''; btnSettings.classList.toggle('active', !isVisible); }); function updateToggleButton() { btnToggle.title = overlaysVisible ? 'Hide overlays' : 'Show overlays'; btnToggle.classList.toggle('inactive', !overlaysVisible); } function showScanning() { container.innerHTML = `