diff --git a/entrypoints/content.ts b/entrypoints/content.ts index 09975a5..be76ab3 100644 --- a/entrypoints/content.ts +++ b/entrypoints/content.ts @@ -204,7 +204,7 @@ export default defineContentScript({ function isExtensionValid(): boolean { try { - return !!chrome.runtime?.id; + return typeof chrome !== 'undefined' && !!chrome.runtime?.id; } catch { return false; } @@ -223,6 +223,25 @@ export default defineContentScript({ setTimeout(() => toast.remove(), 4000); } + // ─── Safe chrome.runtime.sendMessage wrapper ────────────────────────────── + + async function safeSendMessage(payload: Record): Promise { + if (!isExtensionValid()) { + showErrorToast('LexAI was updated — please refresh this page.'); + return null; + } + try { + return await chrome.runtime.sendMessage(payload); + } catch (err) { + if (String(err).includes('Extension context invalidated') || + String(err).includes('message channel closed')) { + showErrorToast('LexAI was updated — please refresh this page.'); + hideToolbar(); + } + return null; + } + } + // ─── LLM call ───────────────────────────────────────────────────────────── async function runAction(action: string) { @@ -251,13 +270,15 @@ export default defineContentScript({ } try { - const response = await chrome.runtime.sendMessage({ + const response = await safeSendMessage({ type: 'ANALYZE_TEXT', payload: { text: textToProcess, action }, - }); + }) as { error?: string; result?: string } | null; hideToolbar(); + if (response === null) return; // safeSendMessage already handled the error + if (response?.error) { showModal(`❌ ${response.error}`, null); } else { @@ -265,8 +286,9 @@ export default defineContentScript({ } } catch (err) { hideToolbar(); - if (String(err).includes('Extension context invalidated')) { - showErrorToast('LexAI was updated. Please refresh the page.'); + if (String(err).includes('Extension context invalidated') || + String(err).includes('message channel closed')) { + showErrorToast('LexAI was updated — please refresh this page.'); } else { showModal(`❌ Error: ${String(err)}`, null); } @@ -440,6 +462,25 @@ export default defineContentScript({ document.body.appendChild(modal); } + // ─── Global extension context error handlers ────────────────────────────── + + window.addEventListener('error', (e) => { + if (e.message?.includes('Extension context invalidated')) { + e.preventDefault(); // suppress console error + hideToolbar(); + showErrorToast('LexAI was updated — please refresh this page.'); + } + }); + + window.addEventListener('unhandledrejection', (e) => { + if (String(e.reason)?.includes('Extension context invalidated') || + String(e.reason)?.includes('message channel closed')) { + e.preventDefault(); + hideToolbar(); + showErrorToast('LexAI was updated — please refresh this page.'); + } + }); + // ─── Event listeners ────────────────────────────────────────────────────── document.addEventListener('mouseup', (e) => { diff --git a/entrypoints/options/Options.tsx b/entrypoints/options/Options.tsx index 11ad017..9ff2758 100644 --- a/entrypoints/options/Options.tsx +++ b/entrypoints/options/Options.tsx @@ -151,6 +151,30 @@ const styles = { } as React.CSSProperties, }; +// ─── Safe chrome storage wrappers ──────────────────────────────────────────── + +function safeStorageGet(keys: string[], callback: (result: Record) => void) { + try { + if (typeof chrome === 'undefined' || !chrome.runtime?.id) return; + chrome.storage.local.get(keys, callback); + } catch (err) { + console.warn('LexAI: Extension context invalidated', err); + } +} + +function safeStorageSet(data: Record, callback?: () => void) { + try { + if (typeof chrome === 'undefined' || !chrome.runtime?.id) return; + if (callback) { + chrome.storage.local.set(data, callback); + } else { + chrome.storage.local.set(data); + } + } catch (err) { + console.warn('LexAI: Extension context invalidated', err); + } +} + // ─── Component ──────────────────────────────────────────────────────────────── function OptionsPage() { @@ -162,7 +186,7 @@ function OptionsPage() { const apiKeyRef = useRef(null); useEffect(() => { - chrome.storage.local.get(["provider", "apiKey", "model"], (result: Record) => { + safeStorageGet(["provider", "apiKey", "model"], (result) => { if (result.provider) setProvider(result.provider); if (result.apiKey) setApiKey(result.apiKey); if (result.model) setModel(result.model); @@ -183,7 +207,7 @@ function OptionsPage() { return; } setSaveStatus('saving'); - chrome.storage.local.set({ provider, apiKey: apiKey.trim(), model }, () => { + safeStorageSet({ provider, apiKey: apiKey.trim(), model }, () => { if (chrome.runtime.lastError) { setSaveStatus('error'); setTimeout(() => setSaveStatus('idle'), 3000); diff --git a/entrypoints/popup/Popup.tsx b/entrypoints/popup/Popup.tsx index 95b44cb..5432938 100644 --- a/entrypoints/popup/Popup.tsx +++ b/entrypoints/popup/Popup.tsx @@ -1,13 +1,24 @@ import React, { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; +// ─── Safe chrome storage wrapper ───────────────────────────────────────────── + +function safeStorageGet(keys: string[], callback: (result: Record) => void) { + try { + if (typeof chrome === 'undefined' || !chrome.runtime?.id) return; + chrome.storage.local.get(keys, callback); + } catch (err) { + console.warn('LexAI: Extension context invalidated', err); + } +} + function Popup() { const [configured, setConfigured] = useState(false); const [provider, setProvider] = useState(''); const [model, setModel] = useState(''); useEffect(() => { - chrome.storage.local.get(["provider", "apiKey", "model"], (result: Record) => { + safeStorageGet(["provider", "apiKey", "model"], (result) => { if (result.apiKey) { setConfigured(true); setProvider(result.provider || 'openai'); @@ -16,7 +27,15 @@ function Popup() { }); }, []); - const openSettings = () => chrome.runtime.openOptionsPage(); + const openSettings = () => { + try { + if (typeof chrome !== 'undefined' && chrome.runtime?.id) { + chrome.runtime.openOptionsPage(); + } + } catch (err) { + console.warn('LexAI: Extension context invalidated', err); + } + }; return (