diff --git a/.eslintrc.json b/.eslintrc.json index 59ca3019..b9087767 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,10 +1,23 @@ { "env": { "browser": true, - "es2021": true + "es2021": true, + "webextensions": true }, "extends": ["eslint:recommended", "plugin:react/recommended"], - "overrides": [], + "overrides": [ + { + "files": [ + "build.mjs", + ".github/workflows/scripts/*.mjs", + "scripts/**/*.js", + "scripts/**/*.mjs" + ], + "env": { + "node": true + } + } + ], "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" diff --git a/package.json b/package.json index 99b0ec87..88d35d41 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "build:safari": "bash ./safari/build.sh", "dev": "node build.mjs --development", "analyze": "node build.mjs --analyze", - "lint": "eslint --ext .js,.mjs,.jsx .", - "lint:fix": "eslint --ext .js,.mjs,.jsx . --fix", + "lint": "npx eslint --ext .js,.mjs,.jsx .", + "lint:fix": "npx eslint --ext .js,.mjs,.jsx . --fix", "pretty": "prettier --write ./**/*.{js,mjs,jsx,json,css,scss}", "stage": "run-script-os", "stage:default": "git add $(git diff --name-only --cached --diff-filter=d)", diff --git a/src/background/index.mjs b/src/background/index.mjs index 9d759b9a..400040e0 100644 --- a/src/background/index.mjs +++ b/src/background/index.mjs @@ -57,203 +57,623 @@ import { generateAnswersWithMoonshotWebApi } from '../services/apis/moonshot-web import { isUsingModelName } from '../utils/model-name-convert.mjs' import { generateAnswersWithDeepSeekApi } from '../services/apis/deepseek-api.mjs' -function setPortProxy(port, proxyTabId) { - port.proxy = Browser.tabs.connect(proxyTabId) - const proxyOnMessage = (msg) => { - port.postMessage(msg) +const RECONNECT_CONFIG = { + MAX_ATTEMPTS: 5, + BASE_DELAY_MS: 1000, // Base delay in milliseconds + BACKOFF_MULTIPLIER: 2, // Multiplier for exponential backoff +} + +const SENSITIVE_KEYWORDS = [ + 'apikey', // Covers apiKey, customApiKey, claudeApiKey, etc. + 'token', // Covers accessToken, refreshToken, etc. + 'secret', + 'password', + 'kimimoonshotrefreshtoken', + 'credential', + 'jwt', + 'session', +] + +function redactSensitiveFields(obj, recursionDepth = 0, maxDepth = 5, seen = new WeakSet()) { + if (recursionDepth > maxDepth) { + // Prevent infinite recursion on circular objects or excessively deep structures + return 'REDACTED_TOO_DEEP' + } + if (obj === null || typeof obj !== 'object') { + return obj } - const portOnMessage = (msg) => { - port.proxy.postMessage(msg) + + if (seen.has(obj)) { + return 'REDACTED_CIRCULAR_REFERENCE' + } + seen.add(obj) + + if (Array.isArray(obj)) { + const redactedArray = [] + for (let i = 0; i < obj.length; i++) { + const item = obj[i] + if (item !== null && typeof item === 'object') { + redactedArray[i] = redactSensitiveFields(item, recursionDepth + 1, maxDepth, seen) + } else { + redactedArray[i] = item + } + } + return redactedArray } - const proxyOnDisconnect = () => { - port.proxy = Browser.tabs.connect(proxyTabId) + + const redactedObj = {} + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const lowerKey = key.toLowerCase() + let isKeySensitive = false + for (const keyword of SENSITIVE_KEYWORDS) { + if (lowerKey.includes(keyword)) { + isKeySensitive = true + break + } + } + if (isKeySensitive) { + redactedObj[key] = 'REDACTED' + } else if (obj[key] !== null && typeof obj[key] === 'object') { + redactedObj[key] = redactSensitiveFields(obj[key], recursionDepth + 1, maxDepth, seen) + } else { + redactedObj[key] = obj[key] + } + } } - const portOnDisconnect = () => { - port.proxy.onMessage.removeListener(proxyOnMessage) - port.onMessage.removeListener(portOnMessage) - port.proxy.onDisconnect.removeListener(proxyOnDisconnect) - port.onDisconnect.removeListener(portOnDisconnect) + return redactedObj +} + +function setPortProxy(port, proxyTabId) { + try { + console.debug(`[background] Attempting to connect to proxy tab: ${proxyTabId}`) + + if (port.proxy) { + try { + if (port._proxyOnMessage) port.proxy.onMessage.removeListener(port._proxyOnMessage) + if (port._proxyOnDisconnect) port.proxy.onDisconnect.removeListener(port._proxyOnDisconnect) + } catch (e) { + console.warn( + '[background] Error removing old listeners from previous port.proxy instance:', + e, + ) + } + } + + if (port._portOnMessage) port.onMessage.removeListener(port._portOnMessage) + if (port._portOnDisconnect) port.onDisconnect.removeListener(port._portOnDisconnect) + + port.proxy = Browser.tabs.connect(proxyTabId, { name: 'background-to-content-script-proxy' }) + console.debug(`[background] Successfully connected to proxy tab: ${proxyTabId}`) + + port._proxyOnMessage = (msg) => { + console.debug('[background] Message from proxy tab:', msg) + if (port._reconnectAttempts) { + port._reconnectAttempts = 0 + console.debug('[background] Reset reconnect attempts after successful proxy message.') + } + port.postMessage(msg) + } + port._portOnMessage = (msg) => { + console.debug('[background] Message to proxy tab:', msg) + if (port.proxy) { + try { + port.proxy.postMessage(msg) + } catch (e) { + console.error( + '[background] Error posting message to proxy tab in _portOnMessage:', + e, + msg, + ) + try { + // Attempt to notify the original sender about the failure + port.postMessage({ + error: + 'Failed to forward message to target tab. Tab might be closed or an extension error occurred.', + }) + } catch (notifyError) { + console.error( + '[background] Error sending forwarding failure notification back to original sender:', + notifyError, + ) + } + } + } else { + console.warn('[background] Port proxy not available to send message:', msg) + } + } + + port._proxyOnDisconnect = () => { + console.warn(`[background] Proxy tab ${proxyTabId} disconnected.`) + + const proxyRef = port.proxy + port.proxy = null + + if (proxyRef) { + if (port._proxyOnMessage) { + try { + proxyRef.onMessage.removeListener(port._proxyOnMessage) + } catch (e) { + console.warn( + '[background] Error removing _proxyOnMessage from disconnected proxyRef:', + e, + ) + } + } + if (port._proxyOnDisconnect) { + try { + proxyRef.onDisconnect.removeListener(port._proxyOnDisconnect) + } catch (e) { + console.warn( + '[background] Error removing _proxyOnDisconnect from disconnected proxyRef:', + e, + ) + } + } + } + + port._reconnectAttempts = (port._reconnectAttempts || 0) + 1 + if (port._reconnectAttempts > RECONNECT_CONFIG.MAX_ATTEMPTS) { + console.error( + `[background] Max reconnect attempts (${RECONNECT_CONFIG.MAX_ATTEMPTS}) reached for tab ${proxyTabId}. Giving up.`, + ) + if (port._portOnMessage) { + try { + port.onMessage.removeListener(port._portOnMessage) + } catch (e) { + console.warn('[background] Error removing _portOnMessage on max retries:', e) + } + } + if (port._portOnDisconnect) { + try { + port.onDisconnect.removeListener(port._portOnDisconnect) + } catch (e) { + console.warn('[background] Error removing _portOnDisconnect on max retries:', e) + } + } + try { + port.postMessage({ + error: `Connection to ChatGPT tab lost after ${RECONNECT_CONFIG.MAX_ATTEMPTS} attempts. Please refresh the page.`, + }) + } catch (e) { + console.warn('[background] Error sending final error message on max retries:', e) + } + return + } + + const delay = + Math.pow(RECONNECT_CONFIG.BACKOFF_MULTIPLIER, port._reconnectAttempts - 1) * + RECONNECT_CONFIG.BASE_DELAY_MS + console.log( + `[background] Attempting reconnect #${port._reconnectAttempts} in ${ + delay / 1000 + }s for tab ${proxyTabId}.`, + ) + + setTimeout(async () => { + try { + await Browser.tabs.get(proxyTabId) + } catch (error) { + console.warn( + `[background] Proxy tab ${proxyTabId} no longer exists. Aborting reconnect.`, + error, + ) + return + } + console.debug( + `[background] Retrying connection to tab ${proxyTabId}, attempt ${port._reconnectAttempts}.`, + ) + setPortProxy(port, proxyTabId) + }, delay) + } + + port._portOnDisconnect = () => { + console.log( + '[background] Main port disconnected (e.g. popup/sidebar closed). Cleaning up proxy connections and listeners.', + ) + if (port._portOnMessage) { + try { + port.onMessage.removeListener(port._portOnMessage) + } catch (e) { + console.warn('[background] Error removing _portOnMessage on main port disconnect:', e) + } + } + const proxyRef = port.proxy + if (proxyRef) { + if (port._proxyOnMessage) { + try { + proxyRef.onMessage.removeListener(port._proxyOnMessage) + } catch (e) { + console.warn( + '[background] Error removing _proxyOnMessage from proxyRef on main port disconnect:', + e, + ) + } + } + if (port._proxyOnDisconnect) { + try { + proxyRef.onDisconnect.removeListener(port._proxyOnDisconnect) + } catch (e) { + console.warn( + '[background] Error removing _proxyOnDisconnect from proxyRef on main port disconnect:', + e, + ) + } + } + try { + proxyRef.disconnect() + } catch (e) { + console.warn('[background] Error disconnecting proxyRef on main port disconnect:', e) + } + port.proxy = null + } + if (port._portOnDisconnect) { + try { + port.onDisconnect.removeListener(port._portOnDisconnect) + } catch (e) { + console.warn('[background] Error removing _portOnDisconnect on main port disconnect:', e) + } + } + port._reconnectAttempts = 0 + } + + port.proxy.onMessage.addListener(port._proxyOnMessage) + port.onMessage.addListener(port._portOnMessage) + port.proxy.onDisconnect.addListener(port._proxyOnDisconnect) + port.onDisconnect.addListener(port._portOnDisconnect) + } catch (error) { + console.error(`[background] Error in setPortProxy for tab ${proxyTabId}:`, error) } - port.proxy.onMessage.addListener(proxyOnMessage) - port.onMessage.addListener(portOnMessage) - port.proxy.onDisconnect.addListener(proxyOnDisconnect) - port.onDisconnect.addListener(portOnDisconnect) } async function executeApi(session, port, config) { - console.debug('modelName', session.modelName) - console.debug('apiMode', session.apiMode) - if (isUsingCustomModel(session)) { - if (!session.apiMode) - await generateAnswersWithCustomApi( + console.log( + `[background] executeApi called for model: ${session.modelName}, apiMode: ${session.apiMode}`, + ) + console.debug('[background] Full session details (redacted):', redactSensitiveFields(session)) + console.debug('[background] Full config details (redacted):', redactSensitiveFields(config)) + if (session.apiMode) { + console.debug( + '[background] Session apiMode details (redacted):', + redactSensitiveFields(session.apiMode), + ) + } + try { + if (isUsingCustomModel(session)) { + console.debug('[background] Using Custom Model API') + if (!session.apiMode) + await generateAnswersWithCustomApi( + port, + session.question, + session, + config.customModelApiUrl.trim() || 'http://localhost:8000/v1/chat/completions', + config.customApiKey, + config.customModelName, + ) + else + await generateAnswersWithCustomApi( + port, + session.question, + session, + session.apiMode.customUrl.trim() || + config.customModelApiUrl.trim() || + 'http://localhost:8000/v1/chat/completions', + session.apiMode.apiKey.trim() || config.customApiKey, + session.apiMode.customName, + ) + } else if (isUsingChatgptWebModel(session)) { + console.debug('[background] Using ChatGPT Web Model') + let tabId + if ( + config.chatgptTabId && + config.customChatGptWebApiUrl === defaultConfig.customChatGptWebApiUrl + ) { + try { + const tab = await Browser.tabs.get(config.chatgptTabId) + if (tab) tabId = tab.id + } catch (e) { + console.warn( + `[background] Failed to get ChatGPT tab with ID ${config.chatgptTabId}:`, + e.message, + ) + } + } + if (tabId) { + console.debug(`[background] ChatGPT Tab ID ${tabId} found.`) + if (!port.proxy) { + console.debug('[background] port.proxy not found, calling setPortProxy.') + setPortProxy(port, tabId) + } + if (port.proxy) { + console.debug('[background] Posting message to proxy tab:', { session }) + try { + port.proxy.postMessage({ session }) + } catch (e) { + console.warn( + '[background] Error posting message to existing proxy tab in executeApi (ChatGPT Web Model):', + e, + '. Attempting to reconnect.', + { session }, + ) + setPortProxy(port, tabId) + if (port.proxy) { + console.debug('[background] Proxy re-established. Attempting to post message again.') + try { + port.proxy.postMessage({ session }) + console.info('[background] Successfully posted session after proxy reconnection.') + } catch (e2) { + console.error( + '[background] Error posting message even after proxy reconnection:', + e2, + { session }, + ) + try { + port.postMessage({ + error: + 'Failed to communicate with ChatGPT tab after reconnection attempt. Try refreshing the page.', + }) + } catch (notifyError) { + console.error( + '[background] Error sending final communication failure notification back:', + notifyError, + ) + } + } + } else { + console.error( + '[background] Failed to re-establish proxy connection. Cannot send session.', + ) + try { + port.postMessage({ + error: + 'Could not re-establish connection to ChatGPT tab. Try refreshing the page.', + }) + } catch (notifyError) { + console.error( + '[background] Error sending re-establishment failure notification back:', + notifyError, + ) + } + } + } + } else { + console.error( + '[background] Failed to send message: port.proxy is still not available after initial setPortProxy attempt.', + ) + try { + port.postMessage({ + error: 'Failed to initialize connection to ChatGPT tab. Try refreshing the page.', + }) + } catch (notifyError) { + console.error( + '[background] Error sending initial connection failure notification back:', + notifyError, + ) + } + } + } else { + console.debug('[background] No valid ChatGPT Tab ID found. Using direct API call.') + const accessToken = await getChatGptAccessToken() + await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken) + } + } else if (isUsingClaudeWebModel(session)) { + console.debug('[background] Using Claude Web Model') + const sessionKey = await getClaudeSessionKey() + await generateAnswersWithClaudeWebApi(port, session.question, session, sessionKey) + } else if (isUsingMoonshotWebModel(session)) { + console.debug('[background] Using Moonshot Web Model') + await generateAnswersWithMoonshotWebApi(port, session.question, session, config) + } else if (isUsingBingWebModel(session)) { + console.debug('[background] Using Bing Web Model') + const accessToken = await getBingAccessToken() + if (isUsingModelName('bingFreeSydney', session)) { + console.debug('[background] Using Bing Free Sydney model') + await generateAnswersWithBingWebApi(port, session.question, session, accessToken, true) + } else { + await generateAnswersWithBingWebApi(port, session.question, session, accessToken) + } + } else if (isUsingGeminiWebModel(session)) { + console.debug('[background] Using Gemini Web Model') + const cookies = await getBardCookies() + await generateAnswersWithBardWebApi(port, session.question, session, cookies) + } else if (isUsingChatgptApiModel(session)) { + console.debug('[background] Using ChatGPT API Model') + await generateAnswersWithChatgptApi(port, session.question, session, config.apiKey) + } else if (isUsingClaudeApiModel(session)) { + console.debug('[background] Using Claude API Model') + await generateAnswersWithClaudeApi(port, session.question, session) + } else if (isUsingMoonshotApiModel(session)) { + console.debug('[background] Using Moonshot API Model') + await generateAnswersWithMoonshotCompletionApi( port, session.question, session, - config.customModelApiUrl.trim() || 'http://localhost:8000/v1/chat/completions', - config.customApiKey, - config.customModelName, + config.moonshotApiKey, ) - else - await generateAnswersWithCustomApi( + } else if (isUsingChatGLMApiModel(session)) { + console.debug('[background] Using ChatGLM API Model') + await generateAnswersWithChatGLMApi(port, session.question, session) + } else if (isUsingDeepSeekApiModel(session)) { + console.debug('[background] Using DeepSeek API Model') + await generateAnswersWithDeepSeekApi(port, session.question, session, config.deepSeekApiKey) + } else if (isUsingOllamaApiModel(session)) { + console.debug('[background] Using Ollama API Model') + await generateAnswersWithOllamaApi(port, session.question, session) + } else if (isUsingOpenRouterApiModel(session)) { + console.debug('[background] Using OpenRouter API Model') + await generateAnswersWithOpenRouterApi( port, session.question, session, - session.apiMode.customUrl.trim() || - config.customModelApiUrl.trim() || - 'http://localhost:8000/v1/chat/completions', - session.apiMode.apiKey.trim() || config.customApiKey, - session.apiMode.customName, + config.openRouterApiKey, ) - } else if (isUsingChatgptWebModel(session)) { - let tabId - if ( - config.chatgptTabId && - config.customChatGptWebApiUrl === defaultConfig.customChatGptWebApiUrl - ) { - const tab = await Browser.tabs.get(config.chatgptTabId).catch(() => {}) - if (tab) tabId = tab.id - } - if (tabId) { - if (!port.proxy) { - setPortProxy(port, tabId) - port.proxy.postMessage({ session }) - } + } else if (isUsingAimlApiModel(session)) { + console.debug('[background] Using AIML API Model') + await generateAnswersWithAimlApi(port, session.question, session, config.aimlApiKey) + } else if (isUsingAzureOpenAiApiModel(session)) { + console.debug('[background] Using Azure OpenAI API Model') + await generateAnswersWithAzureOpenaiApi(port, session.question, session) + } else if (isUsingGptCompletionApiModel(session)) { + console.debug('[background] Using GPT Completion API Model') + await generateAnswersWithGptCompletionApi(port, session.question, session, config.apiKey) + } else if (isUsingGithubThirdPartyApiModel(session)) { + console.debug('[background] Using Github Third Party API Model') + await generateAnswersWithWaylaidwandererApi(port, session.question, session) } else { - const accessToken = await getChatGptAccessToken() - await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken) + console.warn('[background] Unknown model or session configuration:', session) + port.postMessage({ error: 'Unknown model configuration' }) } - } else if (isUsingClaudeWebModel(session)) { - const sessionKey = await getClaudeSessionKey() - await generateAnswersWithClaudeWebApi(port, session.question, session, sessionKey) - } else if (isUsingMoonshotWebModel(session)) { - await generateAnswersWithMoonshotWebApi(port, session.question, session, config) - } else if (isUsingBingWebModel(session)) { - const accessToken = await getBingAccessToken() - if (isUsingModelName('bingFreeSydney', session)) - await generateAnswersWithBingWebApi(port, session.question, session, accessToken, true) - else await generateAnswersWithBingWebApi(port, session.question, session, accessToken) - } else if (isUsingGeminiWebModel(session)) { - const cookies = await getBardCookies() - await generateAnswersWithBardWebApi(port, session.question, session, cookies) - } else if (isUsingChatgptApiModel(session)) { - await generateAnswersWithChatgptApi(port, session.question, session, config.apiKey) - } else if (isUsingClaudeApiModel(session)) { - await generateAnswersWithClaudeApi(port, session.question, session) - } else if (isUsingMoonshotApiModel(session)) { - await generateAnswersWithMoonshotCompletionApi( - port, - session.question, - session, - config.moonshotApiKey, - ) - } else if (isUsingChatGLMApiModel(session)) { - await generateAnswersWithChatGLMApi(port, session.question, session) - } else if (isUsingDeepSeekApiModel(session)) { - await generateAnswersWithDeepSeekApi(port, session.question, session, config.deepSeekApiKey) - } else if (isUsingOllamaApiModel(session)) { - await generateAnswersWithOllamaApi(port, session.question, session) - } else if (isUsingOpenRouterApiModel(session)) { - await generateAnswersWithOpenRouterApi(port, session.question, session, config.openRouterApiKey) - } else if (isUsingAimlApiModel(session)) { - await generateAnswersWithAimlApi(port, session.question, session, config.aimlApiKey) - } else if (isUsingAzureOpenAiApiModel(session)) { - await generateAnswersWithAzureOpenaiApi(port, session.question, session) - } else if (isUsingGptCompletionApiModel(session)) { - await generateAnswersWithGptCompletionApi(port, session.question, session, config.apiKey) - } else if (isUsingGithubThirdPartyApiModel(session)) { - await generateAnswersWithWaylaidwandererApi(port, session.question, session) + } catch (error) { + console.error(`[background] Error in executeApi for model ${session.modelName}:`, error) + port.postMessage({ error: error.message || 'An unexpected error occurred in executeApi' }) } } Browser.runtime.onMessage.addListener(async (message, sender) => { - switch (message.type) { - case 'FEEDBACK': { - const token = await getChatGptAccessToken() - await sendMessageFeedback(token, message.data) - break - } - case 'DELETE_CONVERSATION': { - const token = await getChatGptAccessToken() - await deleteConversation(token, message.data.conversationId) - break - } - case 'NEW_URL': { - await Browser.tabs.create({ - url: message.data.url, - pinned: message.data.pinned, - }) - if (message.data.jumpBack) { - await setUserConfig({ - notificationJumpBackTabId: sender.tab.id, + console.debug('[background] Received message:', message, 'from sender:', sender) + try { + switch (message.type) { + case 'FEEDBACK': { + console.log('[background] Processing FEEDBACK message') + const token = await getChatGptAccessToken() + await sendMessageFeedback(token, message.data) + break + } + case 'DELETE_CONVERSATION': { + console.log('[background] Processing DELETE_CONVERSATION message') + const token = await getChatGptAccessToken() + await deleteConversation(token, message.data.conversationId) + break + } + case 'NEW_URL': { + console.log('[background] Processing NEW_URL message:', message.data) + await Browser.tabs.create({ + url: message.data.url, + pinned: message.data.pinned, }) + if (message.data.jumpBack) { + console.debug('[background] Setting jumpBackTabId:', sender.tab?.id) + await setUserConfig({ + notificationJumpBackTabId: sender.tab?.id, + }) + } + break } - break - } - case 'SET_CHATGPT_TAB': { - await setUserConfig({ - chatgptTabId: sender.tab.id, - }) - break - } - case 'ACTIVATE_URL': - await Browser.tabs.update(message.data.tabId, { active: true }) - break - case 'OPEN_URL': - openUrl(message.data.url) - break - case 'OPEN_CHAT_WINDOW': { - const config = await getUserConfig() - const url = Browser.runtime.getURL('IndependentPanel.html') - const tabs = await Browser.tabs.query({ url: url, windowType: 'popup' }) - if (!config.alwaysCreateNewConversationWindow && tabs.length > 0) - await Browser.windows.update(tabs[0].windowId, { focused: true }) - else - await Browser.windows.create({ - url: url, - type: 'popup', - width: 500, - height: 650, + case 'SET_CHATGPT_TAB': { + console.log('[background] Processing SET_CHATGPT_TAB message. Tab ID:', sender.tab?.id) + await setUserConfig({ + chatgptTabId: sender.tab?.id, }) - break - } - case 'REFRESH_MENU': - refreshMenu() - break - case 'PIN_TAB': { - let tabId - if (message.data.tabId) tabId = message.data.tabId - else tabId = sender.tab.id - - await Browser.tabs.update(tabId, { pinned: true }) - if (message.data.saveAsChatgptConfig) { - await setUserConfig({ chatgptTabId: tabId }) + break } - break - } - case 'FETCH': { - if (message.data.input.includes('bing.com')) { - const accessToken = await getBingAccessToken() - await setUserConfig({ bingAccessToken: accessToken }) + case 'ACTIVATE_URL': + console.log('[background] Processing ACTIVATE_URL message:', message.data) + await Browser.tabs.update(message.data.tabId, { active: true }) + break + case 'OPEN_URL': + console.log('[background] Processing OPEN_URL message:', message.data) + openUrl(message.data.url) + break + case 'OPEN_CHAT_WINDOW': { + console.log('[background] Processing OPEN_CHAT_WINDOW message') + const config = await getUserConfig() + const url = Browser.runtime.getURL('IndependentPanel.html') + const tabs = await Browser.tabs.query({ url: url, windowType: 'popup' }) + if (!config.alwaysCreateNewConversationWindow && tabs.length > 0) { + console.debug('[background] Focusing existing chat window:', tabs[0].windowId) + await Browser.windows.update(tabs[0].windowId, { focused: true }) + } else { + console.debug('[background] Creating new chat window.') + await Browser.windows.create({ + url: url, + type: 'popup', + width: 500, + height: 650, + }) + } + break + } + case 'REFRESH_MENU': + console.log('[background] Processing REFRESH_MENU message') + refreshMenu() + break + case 'PIN_TAB': { + console.log('[background] Processing PIN_TAB message:', message.data) + let tabId = message.data.tabId || sender.tab?.id + if (tabId) { + await Browser.tabs.update(tabId, { pinned: true }) + if (message.data.saveAsChatgptConfig) { + console.debug('[background] Saving pinned tab as ChatGPT config tab:', tabId) + await setUserConfig({ chatgptTabId: tabId }) + } + } else { + console.warn('[background] No tabId found for PIN_TAB message.') + } + break } + case 'FETCH': { + console.log('[background] Processing FETCH message for URL:', message.data.input) + if (message.data.input.includes('bing.com')) { + console.debug('[background] Fetching Bing access token for FETCH message.') + const accessToken = await getBingAccessToken() + await setUserConfig({ bingAccessToken: accessToken }) + } - try { - const response = await fetch(message.data.input, message.data.init) - const text = await response.text() - return [ - { + try { + const response = await fetch(message.data.input, message.data.init) + const text = await response.text() + const responseObject = { + // Defined for clarity before conditional error property body: text, + ok: response.ok, status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers), - }, - null, - ] - } catch (error) { - return [null, error] + } + if (!response.ok) { + responseObject.error = `HTTP error ${response.status}: ${response.statusText}` + console.warn( + `[background] FETCH received error status: ${response.status} for ${message.data.input}`, + ) + } + console.debug( + `[background] FETCH successful for ${message.data.input}, status: ${response.status}`, + ) + return [responseObject, null] + } catch (error) { + console.error(`[background] FETCH error for ${message.data.input}:`, error) + return [null, { message: error.message, stack: error.stack }] + } } + case 'GET_COOKIE': { + console.log('[background] Processing GET_COOKIE message:', message.data) + try { + const cookie = await Browser.cookies.get({ + url: message.data.url, + name: message.data.name, + }) + console.debug('[background] Cookie found:', cookie) + return cookie?.value + } catch (error) { + console.error( + `[background] Error getting cookie ${message.data.name} for ${message.data.url}:`, + error, + ) + return null + } + } + default: + console.warn('[background] Unknown message type received:', message.type) } - case 'GET_COOKIE': { - return (await Browser.cookies.get({ url: message.data.url, name: message.data.name }))?.value + } catch (error) { + console.error( + `[background] Error processing message type ${message.type}:`, + error, + 'Original message:', + message, + ) + if (message.type === 'FETCH') { + return [null, { message: error.message, stack: error.stack }] } } }) @@ -261,22 +681,36 @@ Browser.runtime.onMessage.addListener(async (message, sender) => { try { Browser.webRequest.onBeforeRequest.addListener( (details) => { - if ( - details.url.includes('/public_key') && - !details.url.includes(defaultConfig.chatgptArkoseReqParams) - ) { - let formData = new URLSearchParams() - for (const k in details.requestBody.formData) { - formData.append(k, details.requestBody.formData[k]) - } - setUserConfig({ - chatgptArkoseReqUrl: details.url, - chatgptArkoseReqForm: + try { + console.debug('[background] onBeforeRequest triggered for URL:', details.url) + if ( + details.url.includes('/public_key') && + !details.url.includes(defaultConfig.chatgptArkoseReqParams) + ) { + console.log('[background] Capturing Arkose public_key request:', details.url) + let formData = new URLSearchParams() + if (details.requestBody?.formData) { + for (const k in details.requestBody.formData) { + formData.append(k, details.requestBody.formData[k]) + } + } + const formString = formData.toString() || - new TextDecoder('utf-8').decode(new Uint8Array(details.requestBody.raw[0].bytes)), - }).then(() => { - console.log('Arkose req url and form saved') - }) + (details.requestBody?.raw?.[0]?.bytes + ? new TextDecoder('utf-8').decode(new Uint8Array(details.requestBody.raw[0].bytes)) + : '') + + setUserConfig({ + chatgptArkoseReqUrl: details.url, + chatgptArkoseReqForm: formString, + }) + .then(() => { + console.log('[background] Arkose req url and form saved successfully.') + }) + .catch((e) => console.error('[background] Error saving Arkose req url and form:', e)) + } + } catch (error) { + console.error('[background] Error in onBeforeRequest listener callback:', error, details) } }, { @@ -288,21 +722,41 @@ try { Browser.webRequest.onBeforeSendHeaders.addListener( (details) => { - const headers = details.requestHeaders - for (let i = 0; i < headers.length; i++) { - if (headers[i].name === 'Origin') { - headers[i].value = 'https://www.bing.com' - } else if (headers[i].name === 'Referer') { - headers[i].value = 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx' + try { + console.debug('[background] onBeforeSendHeaders triggered for URL:', details.url) + const headers = details.requestHeaders + let modified = false + for (let i = 0; i < headers.length; i++) { + if (!headers[i]) { + continue + } + const headerNameLower = headers[i].name?.toLowerCase() + if (headerNameLower === 'origin') { + headers[i].value = 'https://www.bing.com' + modified = true + } else if (headerNameLower === 'referer') { + headers[i].value = 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx' + modified = true + } } + if (modified) { + console.debug('[background] Modified headers for Bing:', headers) + } + return { requestHeaders: headers } + } catch (error) { + console.error( + '[background] Error in onBeforeSendHeaders listener callback:', + error, + details, + ) + return { requestHeaders: details.requestHeaders } } - return { requestHeaders: headers } }, { urls: ['wss://sydney.bing.com/*', 'https://www.bing.com/*'], types: ['xmlhttprequest', 'websocket'], }, - ['requestHeaders'], + ['requestHeaders', ...(Browser.runtime.getManifest().manifest_version < 3 ? ['blocking'] : [])], ) Browser.webRequest.onBeforeSendHeaders.addListener( @@ -325,18 +779,109 @@ try { ) Browser.tabs.onUpdated.addListener(async (tabId, info, tab) => { - if (!tab.url) return - // eslint-disable-next-line no-undef - await chrome.sidePanel.setOptions({ - tabId, - path: 'IndependentPanel.html', - enabled: true, - }) + const outerTryCatchError = (error) => { + console.error( + '[background] Error in tabs.onUpdated listener callback (outer):', + error, + tabId, + info, + ) + } + try { + if (!tab.url || (info.status && info.status !== 'complete')) { + console.debug( + `[background] Skipping side panel update for tabId: ${tabId}. Tab URL: ${tab.url}, Info Status: ${info.status}`, + ) + return + } + console.debug( + `[background] tabs.onUpdated event for tabId: ${tabId}, status: ${info.status}, url: ${tab.url}. Proceeding with side panel update.`, + ) + + let sidePanelSet = false + try { + if (Browser.sidePanel && typeof Browser.sidePanel.setOptions === 'function') { + await Browser.sidePanel.setOptions({ + tabId, + path: 'IndependentPanel.html', + enabled: true, + }) + console.debug( + `[background] Side panel options set for tab ${tabId} using Browser.sidePanel`, + ) + sidePanelSet = true + } + } catch (browserError) { + console.warn('[background] Browser.sidePanel.setOptions failed:', browserError.message) + } + + if (!sidePanelSet) { + console.debug('[background] Attempting chrome.sidePanel.setOptions as fallback.') + const chromeApi = globalThis.chrome + if (chromeApi?.sidePanel && typeof chromeApi.sidePanel.setOptions === 'function') { + try { + await chromeApi.sidePanel.setOptions({ + tabId, + path: 'IndependentPanel.html', + enabled: true, + }) + console.debug( + `[background] Side panel options set for tab ${tabId} using chrome.sidePanel`, + ) + sidePanelSet = true + } catch (chromeError) { + console.error( + '[background] chrome.sidePanel.setOptions also failed:', + chromeError.message, + ) + } + } + } + + if (!sidePanelSet) { + console.warn( + '[background] SidePanel API (Browser.sidePanel or chrome.sidePanel) not available or setOptions failed in this browser. Side panel options not set for tab:', + tabId, + ) + } + } catch (error) { + outerTryCatchError(error) + } + }) +} catch (error) { + console.error('[background] Error setting up webRequest or tabs listeners:', error) +} + +try { + registerPortListener(async (session, port, config) => { + console.debug( + `[background] Port listener triggered for session: ${session.modelName}, port: ${port.name}`, + ) + try { + await executeApi(session, port, config) + } catch (e) { + console.error( + `[background] Error in port listener callback executing API for session ${session.modelName}:`, + e, + ) + port.postMessage({ error: e.message || 'An unexpected error occurred in port listener' }) + } }) + console.log('[background] Port listener registered successfully.') } catch (error) { - console.log(error) + console.error('[background] Error registering port listener:', error) } -registerPortListener(async (session, port, config) => await executeApi(session, port, config)) -registerCommands() -refreshMenu() +try { + registerCommands() + console.log('[background] Commands registered successfully.') +} catch (error) { + console.error('[background] Error registering commands:', error) +} + +try { + refreshMenu() + console.log('[background] Menu refreshed successfully.') +} catch (error) { + console.error('[background] Error refreshing menu:', error) +} diff --git a/src/content-script/index.jsx b/src/content-script/index.jsx index 01b0b3aa..33f416ca 100644 --- a/src/content-script/index.jsx +++ b/src/content-script/index.jsx @@ -40,463 +40,983 @@ async function mountComponent(siteName, siteConfig) { return } - const userConfig = await getUserConfig() - - if (!userConfig.alwaysFloatingSidebar) { - const retry = 10 - let oldUrl = location.href - for (let i = 1; i <= retry; i++) { - if (location.href !== oldUrl) { - console.log(`SiteAdapters Retry ${i}/${retry}: stop`) - return + console.debug('[content] mountComponent called with siteConfig:', siteConfig) + try { + const userConfig = await getUserConfig() + console.debug('[content] User config in mountComponent:', userConfig) + + if (!userConfig.alwaysFloatingSidebar) { + const retry = 10 + let oldUrl = location.href + let elementFound = false + for (let i = 1; i <= retry; i++) { + console.debug(`[content] mountComponent retry ${i}/${retry} for element detection.`) + if (location.href !== oldUrl) { + console.log('[content] URL changed during retry, stopping mountComponent.') + return + } + const e = + (siteConfig && + (getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery) || + getPossibleElementByQuerySelector(siteConfig.appendContainerQuery) || + getPossibleElementByQuerySelector(siteConfig.resultsContainerQuery))) || + getPossibleElementByQuerySelector([userConfig.prependQuery]) || + getPossibleElementByQuerySelector([userConfig.appendQuery]) + if (e) { + console.log('[content] Element found for mounting component:', e) + elementFound = true + break + } else { + console.debug(`[content] Element not found on retry ${i}.`) + if (i === retry) { + console.warn('[content] Element not found after all retries for mountComponent.') + return + } + await new Promise((r) => setTimeout(r, 500)) + } } - const e = - (siteConfig && - (getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery) || - getPossibleElementByQuerySelector(siteConfig.appendContainerQuery) || - getPossibleElementByQuerySelector(siteConfig.resultsContainerQuery))) || - getPossibleElementByQuerySelector([userConfig.prependQuery]) || - getPossibleElementByQuerySelector([userConfig.appendQuery]) - if (e) { - console.log(`SiteAdapters Retry ${i}/${retry}: found`) - console.log(e) - break - } else { - console.log(`SiteAdapters Retry ${i}/${retry}: not found`) - if (i === retry) return - else await new Promise((r) => setTimeout(r, 500)) + if (!elementFound) { + console.warn('[content] No suitable element found for non-floating sidebar after retries.') + return } } - } - document.querySelectorAll('.chatgptbox-container,#chatgptbox-container').forEach((e) => { - unmountComponentAtNode(e) - e.remove() - }) - let question - if (userConfig.inputQuery) question = await getInput([userConfig.inputQuery]) - if (!question && siteConfig) question = await getInput(siteConfig.inputQuery) + document.querySelectorAll('.chatgptbox-container,#chatgptbox-container').forEach((e) => { + try { + unmountComponentAtNode(e) + e.remove() + } catch (err) { + console.error('[content] Error removing existing chatgptbox container:', err) + } + }) - document.querySelectorAll('.chatgptbox-container,#chatgptbox-container').forEach((e) => { - unmountComponentAtNode(e) - e.remove() - }) + let question + if (userConfig.inputQuery) { + console.debug('[content] Getting input from userConfig.inputQuery') + question = await getInput([userConfig.inputQuery]) + } + if (!question && siteConfig) { + console.debug('[content] Getting input from siteConfig.inputQuery') + question = await getInput(siteConfig.inputQuery) + } + console.debug('[content] Question for component:', question) + + // Ensure cleanup again in case getInput took time and new elements were added + document.querySelectorAll('.chatgptbox-container,#chatgptbox-container').forEach((e) => { + try { + unmountComponentAtNode(e) + e.remove() + } catch (err) { + console.error('[content] Error removing existing chatgptbox container post getInput:', err) + } + }) - if (userConfig.alwaysFloatingSidebar && question) { - const position = { - x: window.innerWidth - 300 - Math.floor((20 / 100) * window.innerWidth), - y: window.innerHeight / 2 - 200, + if (userConfig.alwaysFloatingSidebar && question) { + console.log('[content] Rendering floating sidebar.') + const position = { + x: window.innerWidth - 300 - Math.floor((20 / 100) * window.innerWidth), + y: window.innerHeight / 2 - 200, + } + const toolbarContainer = createElementAtPosition(position.x, position.y) + toolbarContainer.className = 'chatgptbox-toolbar-container-not-queryable' + + let triggered = false + if (userConfig.triggerMode === 'always') triggered = true + else if ( + userConfig.triggerMode === 'questionMark' && + question && + endsWithQuestionMark(question.trim()) + ) + triggered = true + console.debug('[content] Floating sidebar triggered:', triggered) + + render( + , + toolbarContainer, + ) + console.log('[content] Floating sidebar rendered.') + return } - const toolbarContainer = createElementAtPosition(position.x, position.y) - toolbarContainer.className = 'chatgptbox-toolbar-container-not-queryable' - let triggered = false - if (userConfig.triggerMode === 'always') triggered = true - else if (userConfig.triggerMode === 'questionMark' && endsWithQuestionMark(question.trim())) - triggered = true + if (!question && !userConfig.alwaysFloatingSidebar) { + console.log( + '[content] No question found and not alwaysFloatingSidebar, skipping DecisionCard render.', + ) + return + } + console.log('[content] Rendering DecisionCard.') + const container = document.createElement('div') + container.id = 'chatgptbox-container' + if (siteName === 'google' || siteName === 'kagi') { + container.style.width = '350px' + } render( - , - toolbarContainer, + container, ) - return + console.log('[content] DecisionCard rendered.') + } catch (error) { + console.error('[content] Error in mountComponent:', error) } - - const container = document.createElement('div') - container.id = 'chatgptbox-container' - if (siteName === 'google' || siteName === 'kagi') { - container.style.width = '350px' - } - render( - , - container, - ) } -/** - * @param {string[]|function} inputQuery - * @returns {Promise} - */ async function getInput(inputQuery) { - let input - if (typeof inputQuery === 'function') { - input = await inputQuery() - const replyPromptBelow = `Reply in ${await getPreferredLanguage()}. Regardless of the language of content I provide below. !!This is very important!!` - const replyPromptAbove = `Reply in ${await getPreferredLanguage()}. Regardless of the language of content I provide above. !!This is very important!!` - if (input) return `${replyPromptBelow}\n\n` + input + `\n\n${replyPromptAbove}` - return input - } - const searchInput = getPossibleElementByQuerySelector(inputQuery) - if (searchInput) { - if (searchInput.value) input = searchInput.value - else if (searchInput.textContent) input = searchInput.textContent - if (input) - return ( - `Reply in ${await getPreferredLanguage()}.\nThe following is a search input in a search engine, ` + - `giving useful content or solutions and as much information as you can related to it, ` + - `use markdown syntax to make your answer more readable, such as code blocks, bold, list:\n` + - input - ) + console.debug('[content] getInput called with query:', inputQuery) + try { + let input + if (typeof inputQuery === 'function') { + console.debug('[content] Input query is a function.') + input = await inputQuery() + if (input) { + const preferredLanguage = await getPreferredLanguage() + const replyPromptBelow = `Reply in ${preferredLanguage}. Regardless of the language of content I provide below. !!This is very important!!` + const replyPromptAbove = `Reply in ${preferredLanguage}. Regardless of the language of content I provide above. !!This is very important!!` + const result = `${replyPromptBelow}\n\n${input}\n\n${replyPromptAbove}` + console.debug('[content] getInput from function result:', result) + return result + } + console.debug('[content] getInput from function returned no input.') + return input + } + console.debug('[content] Input query is a selector.') + const searchInput = getPossibleElementByQuerySelector(inputQuery) + if (searchInput) { + console.debug('[content] Found search input element:', searchInput) + if (searchInput.value) input = searchInput.value + else if (searchInput.textContent) input = searchInput.textContent + if (input) { + const preferredLanguage = await getPreferredLanguage() + const result = + `Reply in ${preferredLanguage}.\nThe following is a search input in a search engine, ` + + `giving useful content or solutions and as much information as you can related to it, ` + + `use markdown syntax to make your answer more readable, such as code blocks, bold, list:\n` + + input + console.debug('[content] getInput from selector result:', result) + return result + } + } + console.debug('[content] No input found from selector or element empty.') + return undefined + } catch (error) { + console.error('[content] Error in getInput:', error) + return undefined } } let toolbarContainer const deleteToolbar = () => { - if (toolbarContainer && toolbarContainer.className === 'chatgptbox-toolbar-container') - toolbarContainer.remove() + try { + if (toolbarContainer && toolbarContainer.className === 'chatgptbox-toolbar-container') { + console.debug('[content] Deleting toolbar:', toolbarContainer) + toolbarContainer.remove() + toolbarContainer = null + } + } catch (error) { + console.error('[content] Error in deleteToolbar:', error) + } } -const createSelectionTools = async (toolbarContainer, selection) => { - toolbarContainer.className = 'chatgptbox-toolbar-container' - const userConfig = await getUserConfig() - render( - , - toolbarContainer, +const createSelectionTools = async (toolbarContainerElement, selection) => { + console.debug( + '[content] createSelectionTools called with selection:', + selection, + 'and container:', + toolbarContainerElement, ) + try { + toolbarContainerElement.className = 'chatgptbox-toolbar-container' + const userConfig = await getUserConfig() + render( + , + toolbarContainerElement, + ) + console.log('[content] Selection tools rendered.') + } catch (error) { + console.error('[content] Error in createSelectionTools:', error) + } } async function prepareForSelectionTools() { + console.log('[content] Initializing selection tools.') document.addEventListener('mouseup', (e) => { - if (toolbarContainer && toolbarContainer.contains(e.target)) return - const selectionElement = - window.getSelection()?.rangeCount > 0 && - window.getSelection()?.getRangeAt(0).endContainer.parentElement - if (toolbarContainer && selectionElement && toolbarContainer.contains(selectionElement)) return - - deleteToolbar() - setTimeout(async () => { - const selection = window - .getSelection() - ?.toString() - .trim() - .replace(/^-+|-+$/g, '') - if (selection) { - let position - - const config = await getUserConfig() - if (!config.selectionToolsNextToInputBox) position = { x: e.pageX + 20, y: e.pageY + 20 } - else { - const inputElement = selectionElement.querySelector('input, textarea') - if (inputElement) { - position = getClientPosition(inputElement) - position = { - x: position.x + window.scrollX + inputElement.offsetWidth + 50, - y: e.pageY + 30, + try { + if (toolbarContainer?.contains(e.target)) { + console.debug('[content] Mouseup inside toolbar, ignoring.') + return + } + const selectionElement = + window.getSelection()?.rangeCount > 0 && + window.getSelection()?.getRangeAt(0).endContainer.parentElement + if (selectionElement && toolbarContainer?.contains(selectionElement)) { + console.debug('[content] Mouseup selection is inside toolbar, ignoring.') + return + } + + deleteToolbar() + setTimeout(async () => { + try { + const selection = window + .getSelection() + ?.toString() + .trim() + .replace(/^-+|-+$/g, '') + if (selection) { + console.debug('[content] Text selected:', selection) + let position + + const config = await getUserConfig() + if (!config.selectionToolsNextToInputBox) { + position = { x: e.pageX + 20, y: e.pageY + 20 } + } else { + const activeElement = document.activeElement + const inputElement = + selectionElement?.querySelector('input, textarea') || + (activeElement?.matches('input, textarea') ? activeElement : null) + + if (inputElement) { + console.debug( + '[content] Input element found for positioning toolbar:', + inputElement, + ) + const clientRect = getClientPosition(inputElement) + position = { + x: clientRect.x + window.scrollX + inputElement.offsetWidth + 50, + y: e.pageY + 30, + } + } else { + position = { x: e.pageX + 20, y: e.pageY + 20 } + } } + console.debug('[content] Toolbar position:', position) + toolbarContainer = createElementAtPosition(position.x, position.y) + await createSelectionTools(toolbarContainer, selection) } else { - position = { x: e.pageX + 20, y: e.pageY + 20 } + console.debug('[content] No text selected on mouseup.') } + } catch (err) { + console.error('[content] Error in mouseup setTimeout callback for selection tools:', err) } - toolbarContainer = createElementAtPosition(position.x, position.y) - await createSelectionTools(toolbarContainer, selection) - } - }) + }, 0) + } catch (error) { + console.error('[content] Error in mouseup listener for selection tools:', error) + } }) - document.addEventListener('mousedown', (e) => { - if (toolbarContainer && toolbarContainer.contains(e.target)) return - document.querySelectorAll('.chatgptbox-toolbar-container').forEach((e) => e.remove()) + document.addEventListener('mousedown', (e) => { + try { + if (toolbarContainer?.contains(e.target)) { + console.debug('[content] Mousedown inside toolbar, ignoring.') + return + } + console.debug('[content] Mousedown outside toolbar, removing existing toolbars.') + document.querySelectorAll('.chatgptbox-toolbar-container').forEach((el) => el.remove()) + toolbarContainer = null + } catch (error) { + console.error('[content] Error in mousedown listener for selection tools:', error) + } }) + document.addEventListener('keydown', (e) => { - if ( - toolbarContainer && - !toolbarContainer.contains(e.target) && - (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA') - ) { - setTimeout(() => { - if (!window.getSelection()?.toString().trim()) deleteToolbar() - }) + try { + if ( + toolbarContainer && + !toolbarContainer.contains(e.target) && + (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA') + ) { + console.debug('[content] Keydown in input/textarea outside toolbar.') + setTimeout(() => { + try { + if (!window.getSelection()?.toString().trim()) { + console.debug('[content] No selection after keydown, deleting toolbar.') + deleteToolbar() + } + } catch (err_inner) { + console.error('[content] Error in keydown setTimeout callback:', err_inner) + } + }, 0) + } + } catch (error) { + console.error('[content] Error in keydown listener for selection tools:', error) } }) } async function prepareForSelectionToolsTouch() { + console.log('[content] Initializing touch selection tools.') document.addEventListener('touchend', (e) => { - if (toolbarContainer && toolbarContainer.contains(e.target)) return - if ( - toolbarContainer && - window.getSelection()?.rangeCount > 0 && - toolbarContainer.contains(window.getSelection()?.getRangeAt(0).endContainer.parentElement) - ) - return - - deleteToolbar() - setTimeout(() => { - const selection = window - .getSelection() - ?.toString() - .trim() - .replace(/^-+|-+$/g, '') - if (selection) { - toolbarContainer = createElementAtPosition( - e.changedTouches[0].pageX + 20, - e.changedTouches[0].pageY + 20, - ) - createSelectionTools(toolbarContainer, selection) + try { + if (toolbarContainer?.contains(e.target)) { + console.debug('[content] Touchend inside toolbar, ignoring.') + return } - }) + if ( + window.getSelection()?.rangeCount > 0 && + toolbarContainer?.contains(window.getSelection()?.getRangeAt(0).endContainer.parentElement) + ) { + console.debug('[content] Touchend selection is inside toolbar, ignoring.') + return + } + + deleteToolbar() + setTimeout(async () => { + try { + const selection = window + .getSelection() + ?.toString() + .trim() + .replace(/^-+|-+$/g, '') + if (selection) { + console.debug('[content] Text selected via touch:', selection) + const touch = e.changedTouches[0] + toolbarContainer = createElementAtPosition(touch.pageX + 20, touch.pageY + 20) + await createSelectionTools(toolbarContainer, selection) + } else { + console.debug('[content] No text selected on touchend.') + } + } catch (err) { + console.error( + '[content] Error in touchend setTimeout callback for touch selection tools:', + err, + ) + } + }, 0) + } catch (error) { + console.error('[content] Error in touchend listener for touch selection tools:', error) + } }) - document.addEventListener('touchstart', (e) => { - if (toolbarContainer && toolbarContainer.contains(e.target)) return - document.querySelectorAll('.chatgptbox-toolbar-container').forEach((e) => e.remove()) + document.addEventListener('touchstart', (e) => { + try { + if (toolbarContainer?.contains(e.target)) { + console.debug('[content] Touchstart inside toolbar, ignoring.') + return + } + console.debug('[content] Touchstart outside toolbar, removing existing toolbars.') + document.querySelectorAll('.chatgptbox-toolbar-container').forEach((el) => el.remove()) + toolbarContainer = null + } catch (error) { + console.error('[content] Error in touchstart listener for touch selection tools:', error) + } }) } let menuX, menuY async function prepareForRightClickMenu() { + console.log('[content] Initializing right-click menu handler.') document.addEventListener('contextmenu', (e) => { menuX = e.clientX menuY = e.clientY + console.debug(`[content] Context menu opened at X: ${menuX}, Y: ${menuY}`) }) Browser.runtime.onMessage.addListener(async (message) => { if (message.type === 'CREATE_CHAT') { - const data = message.data - let prompt = '' - if (data.itemId in toolsConfig) { - prompt = await toolsConfig[data.itemId].genPrompt(data.selectionText) - } else if (data.itemId in menuConfig) { - const menuItem = menuConfig[data.itemId] - if (!menuItem.genPrompt) return - else prompt = await menuItem.genPrompt() - if (prompt) prompt = await cropText(`Reply in ${await getPreferredLanguage()}.\n` + prompt) + console.log('[content] Received CREATE_CHAT message:', message) + try { + const data = message.data + let prompt = '' + if (data.itemId in toolsConfig) { + console.debug('[content] Generating prompt from toolsConfig for item:', data.itemId) + prompt = await toolsConfig[data.itemId].genPrompt(data.selectionText) + } else if (data.itemId in menuConfig) { + console.debug('[content] Generating prompt from menuConfig for item:', data.itemId) + const menuItem = menuConfig[data.itemId] + if (!menuItem.genPrompt) { + console.warn('[content] No genPrompt for menu item:', data.itemId) + return + } + prompt = await menuItem.genPrompt() + if (prompt) { + const preferredLanguage = await getPreferredLanguage() + prompt = await cropText(`Reply in ${preferredLanguage}.\n` + prompt) + } + } else { + console.warn('[content] Unknown itemId for CREATE_CHAT:', data.itemId) + return + } + console.debug('[content] Generated prompt:', prompt) + + const position = data.useMenuPosition + ? { x: menuX, y: menuY } + : { x: window.innerWidth / 2 - 300, y: window.innerHeight / 2 - 200 } + console.debug('[content] Toolbar position for CREATE_CHAT:', position) + const container = createElementAtPosition(position.x, position.y) + container.className = 'chatgptbox-toolbar-container-not-queryable' + const userConfig = await getUserConfig() + render( + , + container, + ) + console.log('[content] CREATE_CHAT toolbar rendered.') + } catch (error) { + console.error('[content] Error processing CREATE_CHAT message:', error, message) } - - const position = data.useMenuPosition - ? { x: menuX, y: menuY } - : { x: window.innerWidth / 2 - 300, y: window.innerHeight / 2 - 200 } - const container = createElementAtPosition(position.x, position.y) - container.className = 'chatgptbox-toolbar-container-not-queryable' - const userConfig = await getUserConfig() - render( - , - container, - ) } }) } async function prepareForStaticCard() { - const userConfig = await getUserConfig() - let siteRegex - if (userConfig.useSiteRegexOnly) siteRegex = userConfig.siteRegex - else - siteRegex = new RegExp( - (userConfig.siteRegex && userConfig.siteRegex + '|') + Object.keys(siteConfig).join('|'), - ) - - const matches = location.hostname.match(siteRegex) - if (matches) { - const siteName = matches[0] + console.log('[content] Initializing static card.') + try { + const userConfig = await getUserConfig() + let siteRegexPattern + if (userConfig.useSiteRegexOnly) { + siteRegexPattern = userConfig.siteRegex + } else { + siteRegexPattern = + (userConfig.siteRegex ? userConfig.siteRegex + '|' : '') + + Object.keys(siteConfig) + .filter((k) => k) + .join('|') + } - if ( - userConfig.siteAdapters.includes(siteName) && - !userConfig.activeSiteAdapters.includes(siteName) - ) + if (!siteRegexPattern) { + console.debug('[content] No site regex pattern defined for static card.') return + } + const siteRegex = new RegExp(siteRegexPattern) + console.debug('[content] Static card site regex:', siteRegex) + + const matches = location.hostname.match(siteRegex) + if (matches) { + const siteName = matches[0] + console.log(`[content] Static card matched site: ${siteName}`) + + if ( + userConfig.siteAdapters.includes(siteName) && + !userConfig.activeSiteAdapters.includes(siteName) + ) { + console.log( + `[content] Site adapter for ${siteName} is installed but not active. Skipping static card.`, + ) + return + } - let initSuccess = true - if (siteName in siteConfig) { - const siteAction = siteConfig[siteName].action - if (siteAction && siteAction.init) { - initSuccess = await siteAction.init(location.hostname, userConfig, getInput, mountComponent) + let initSuccess = true + if (siteName in siteConfig) { + const siteAdapterAction = siteConfig[siteName].action + if (siteAdapterAction?.init) { + console.debug(`[content] Initializing site adapter action for ${siteName}.`) + initSuccess = await siteAdapterAction.init( + location.hostname, + userConfig, + getInput, + mountComponent, + ) + console.debug(`[content] Site adapter init success for ${siteName}: ${initSuccess}`) + } } - } - if (initSuccess) mountComponent(siteName, siteConfig[siteName]) + if (initSuccess) { + console.log(`[content] Mounting static card for site: ${siteName}`) + await mountComponent(siteName, siteConfig[siteName]) + } else { + console.warn(`[content] Static card init failed for site: ${siteName}`) + } + } else { + console.debug('[content] No static card match for current site:', location.hostname) + } + } catch (error) { + console.error('[content] Error in prepareForStaticCard:', error) } } async function overwriteAccessToken() { - if (location.hostname !== 'chatgpt.com') { - if (location.hostname === 'kimi.moonshot.cn' || location.hostname.includes('kimi.com')) { - setUserConfig({ - kimiMoonShotRefreshToken: window.localStorage.refresh_token, - }) + console.debug('[content] overwriteAccessToken called for hostname:', location.hostname) + try { + const isKimiHost = + location.hostname === 'kimi.moonshot.cn' || + location.hostname === 'kimi.com' || + location.hostname === 'www.kimi.com' + if (isKimiHost) { + console.log(`[content] On ${location.hostname}, attempting to save refresh token.`) + const refreshToken = window.localStorage.refresh_token + if (refreshToken) { + await setUserConfig({ kimiMoonShotRefreshToken: refreshToken }) + console.log('[content] Kimi Moonshot refresh token saved.') + } else { + console.warn('[content] Kimi Moonshot refresh token not found in localStorage.') + } + return } - return - } - let data - if (location.pathname === '/api/auth/session') { - const response = document.querySelector('pre').textContent - try { - data = JSON.parse(response) - } catch (error) { - console.error('json error', error) + if (location.hostname !== 'chatgpt.com') { + console.debug('[content] Not on chatgpt.com, skipping access token overwrite.') + return } - } else { - const resp = await fetch('https://chatgpt.com/api/auth/session') - data = await resp.json().catch(() => ({})) - } - if (data && data.accessToken) { - await setAccessToken(data.accessToken) - console.log(data.accessToken) - } -} - -async function prepareForForegroundRequests() { - if (location.hostname !== 'chatgpt.com' || location.pathname === '/auth/login') return - const userConfig = await getUserConfig() - - if ( - !chatgptWebModelKeys.some((model) => - getApiModesStringArrayFromConfig(userConfig, true).includes(model), - ) - ) - return - - // if (location.pathname === '/') { - // const input = document.querySelector('#prompt-textarea') - // if (input) { - // input.textContent = ' ' - // input.dispatchEvent(new Event('input', { bubbles: true })) - // setTimeout(() => { - // input.textContent = '' - // input.dispatchEvent(new Event('input', { bubbles: true })) - // }, 300) - // } - // } - - await Browser.runtime.sendMessage({ - type: 'SET_CHATGPT_TAB', - data: {}, - }) + console.log('[content] On chatgpt.com, attempting to overwrite access token.') + let data + if (location.pathname === '/api/auth/session') { + console.debug('[content] On /api/auth/session page.') + const preElement = document.querySelector('pre') + if (preElement?.textContent) { + const response = preElement.textContent + try { + data = JSON.parse(response) + console.debug('[content] Parsed access token data from
 tag.')
+        } catch (error) {
+          console.error('[content] Failed to parse JSON from 
 tag for access token:', error)
+        }
+      } else {
+        console.warn(
+          '[content] 
 tag not found or empty for access token on /api/auth/session.',
+        )
+      }
+    } else {
+      console.debug('[content] Not on /api/auth/session page, fetching token from API endpoint.')
+      try {
+        const resp = await fetch('https://chatgpt.com/api/auth/session')
+        if (resp.ok) {
+          data = await resp.json()
+          console.debug('[content] Fetched access token data from API endpoint.')
+        } else {
+          console.warn(
+            `[content] Failed to fetch access token, status: ${resp.status}`,
+            await resp.text(),
+          )
+        }
+      } catch (error) {
+        console.error('[content] Error fetching access token from API:', error)
+      }
+    }
 
-  registerPortListener(async (session, port) => {
-    if (isUsingChatgptWebModel(session)) {
-      const accessToken = await getChatGptAccessToken()
-      await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken)
+    if (data?.accessToken) {
+      await setAccessToken(data.accessToken)
+      console.log('[content] ChatGPT Access token has been set successfully from page data.')
+    } else {
+      console.warn('[content] No access token found in page data or fetch response.')
     }
-  })
+  } catch (error) {
+    console.error('[content] Error in overwriteAccessToken:', error)
+  }
 }
 
 async function getClaudeSessionKey() {
-  return Browser.runtime.sendMessage({
-    type: 'GET_COOKIE',
-    data: { url: 'https://claude.ai/', name: 'sessionKey' },
-  })
+  console.debug('[content] getClaudeSessionKey called.')
+  try {
+    const sessionKey = await Browser.runtime.sendMessage({
+      type: 'GET_COOKIE',
+      data: { url: 'https://claude.ai/', name: 'sessionKey' },
+    })
+    console.debug(
+      '[content] Claude session key from background:',
+      sessionKey ? 'found' : 'not found',
+    )
+    return sessionKey
+  } catch (error) {
+    console.error('[content] Error in getClaudeSessionKey sending message:', error)
+    return null
+  }
 }
 
 async function prepareForJumpBackNotification() {
-  if (
-    location.hostname === 'chatgpt.com' &&
-    document.querySelector('button[data-testid=login-button]')
-  ) {
-    console.log('chatgpt not logged in')
-    return
-  }
+  console.log('[content] Initializing jump back notification.')
+  try {
+    if (
+      location.hostname === 'chatgpt.com' &&
+      document.querySelector('button[data-testid=login-button]')
+    ) {
+      console.log('[content] ChatGPT login button found, user not logged in. Skipping jump back.')
+      return
+    }
 
-  const url = new URL(window.location.href)
-  if (url.searchParams.has('chatgptbox_notification')) {
-    if (location.hostname === 'claude.ai' && !(await getClaudeSessionKey())) {
-      console.log('claude not logged in')
-
-      await new Promise((resolve) => {
-        const timer = setInterval(async () => {
-          const token = await getClaudeSessionKey()
-          if (token) {
-            clearInterval(timer)
-            resolve()
+    const url = new URL(window.location.href)
+    if (url.searchParams.has('chatgptbox_notification')) {
+      console.log('[content] chatgptbox_notification param found in URL.')
+
+      if (location.hostname === 'claude.ai') {
+        console.debug('[content] On claude.ai, checking login status.')
+        let claudeSession = await getClaudeSessionKey()
+        if (!claudeSession) {
+          console.log('[content] Claude session key not found, waiting for it...')
+          let promiseSettled = false
+          let timerId = null
+          let timeoutId = null
+          const cleanup = () => {
+            if (timerId) clearInterval(timerId)
+            if (timeoutId) clearTimeout(timeoutId)
           }
-        }, 500)
-      })
-    }
 
-    if (
-      (location.hostname === 'kimi.moonshot.cn' || location.hostname.includes('kimi.com')) &&
-      !window.localStorage.refresh_token
-    ) {
-      console.log('kimi not logged in')
-      setTimeout(() => {
-        document.querySelector('.user-info-container').click()
-      }, 1000)
-
-      await new Promise((resolve) => {
-        const timer = setInterval(() => {
-          const token = window.localStorage.refresh_token
-          if (token) {
-            setUserConfig({
-              kimiMoonShotRefreshToken: token,
+          try {
+            await new Promise((resolve, reject) => {
+              timerId = setInterval(async () => {
+                if (promiseSettled) {
+                  console.warn(
+                    '[content] Promise already settled but Claude interval still running. This indicates a potential cleanup issue.',
+                  )
+                  cleanup()
+                  return
+                }
+                try {
+                  claudeSession = await getClaudeSessionKey()
+                  if (claudeSession) {
+                    if (!promiseSettled) {
+                      promiseSettled = true
+                      cleanup()
+                      console.log('[content] Claude session key found after waiting.')
+                      resolve()
+                    }
+                  }
+                } catch (err) {
+                  console.error('[content] Error polling for Claude session key:', err)
+                  const errMsg = err.message.toLowerCase()
+                  if (
+                    (errMsg.includes('network') || errMsg.includes('permission')) &&
+                    !promiseSettled
+                  ) {
+                    promiseSettled = true
+                    cleanup()
+                    reject(new Error(`Failed to get Claude session key due to: ${err.message}`))
+                  }
+                }
+              }, 500)
+
+              timeoutId = setTimeout(() => {
+                if (!promiseSettled) {
+                  promiseSettled = true
+                  cleanup()
+                  console.warn('[content] Timed out waiting for Claude session key.')
+                  reject(new Error('Timed out waiting for Claude session key.'))
+                }
+              }, 30000)
+            })
+          } catch (err) {
+            console.error(
+              '[content] Failed to get Claude session key for jump back notification:',
+              err,
+            )
+            return
+          }
+        } else {
+          console.log('[content] Claude session key found immediately.')
+        }
+      }
+
+      const isKimiHost =
+        location.hostname === 'kimi.moonshot.cn' ||
+        location.hostname === 'kimi.com' ||
+        location.hostname === 'www.kimi.com'
+      if (isKimiHost) {
+        console.debug('[content] On Kimi host, checking login status.')
+        if (!window.localStorage.refresh_token) {
+          console.log('[content] Kimi refresh token not found, attempting to trigger login.')
+          setTimeout(() => {
+            try {
+              document.querySelectorAll('button').forEach((button) => {
+                if (button.textContent === '立即登录') {
+                  console.log('[content] Clicking Kimi login button.')
+                  button.click()
+                }
+              })
+            } catch (err_click) {
+              console.error('[content] Error clicking Kimi login button:', err_click)
+            }
+          }, 1000)
+
+          let promiseSettled = false
+          let timerId = null
+          let timeoutId = null
+          const cleanup = () => {
+            if (timerId) clearInterval(timerId)
+            if (timeoutId) clearTimeout(timeoutId)
+          }
+
+          try {
+            await new Promise((resolve, reject) => {
+              timerId = setInterval(async () => {
+                if (promiseSettled) {
+                  console.warn(
+                    '[content] Promise already settled but Kimi interval still running. This indicates a potential cleanup issue.',
+                  )
+                  cleanup()
+                  return
+                }
+                try {
+                  const token = window.localStorage.refresh_token
+                  if (token) {
+                    if (!promiseSettled) {
+                      promiseSettled = true
+                      cleanup()
+                      console.log('[content] Kimi refresh token found after waiting.')
+                      await setUserConfig({ kimiMoonShotRefreshToken: token })
+                      console.log('[content] Kimi refresh token saved to config.')
+                      resolve()
+                    }
+                  }
+                } catch (err_set) {
+                  console.error('[content] Error setting Kimi refresh token from polling:', err_set)
+                  // Do not reject on polling error, let timeout handle failure.
+                }
+              }, 500)
+
+              timeoutId = setTimeout(() => {
+                if (!promiseSettled) {
+                  promiseSettled = true
+                  cleanup()
+                  console.warn('[content] Timed out waiting for Kimi refresh token.')
+                  reject(new Error('Timed out waiting for Kimi refresh token.'))
+                }
+              }, 30000)
             })
-            clearInterval(timer)
-            resolve()
+          } catch (err) {
+            console.error(
+              '[content] Failed to get Kimi refresh token for jump back notification:',
+              err,
+            )
+            return
           }
-        }, 500)
+        } else {
+          console.log('[content] Kimi refresh token found in localStorage.')
+          await setUserConfig({ kimiMoonShotRefreshToken: window.localStorage.refresh_token })
+        }
+      }
+
+      console.log('[content] Rendering WebJumpBackNotification.')
+      const div = document.createElement('div')
+      document.body.append(div)
+      render(
+        ,
+        div,
+      )
+      console.log('[content] WebJumpBackNotification rendered.')
+    } else {
+      console.debug('[content] No chatgptbox_notification param in URL.')
+    }
+  } catch (error) {
+    console.error('[content] Error in prepareForJumpBackNotification:', error)
+  }
+}
+
+async function run() {
+  console.log('[content] Script run started.')
+  try {
+    await getPreferredLanguageKey()
+      .then((lang) => {
+        console.log(`[content] Setting language to: ${lang}`)
+        changeLanguage(lang)
+      })
+      .catch((err) => console.error('[content] Error setting preferred language:', err))
+
+    Browser.runtime.onMessage.addListener(async (message) => {
+      console.debug('[content] Received runtime message:', message)
+      try {
+        if (message.type === 'CHANGE_LANG') {
+          console.log('[content] Processing CHANGE_LANG message:', message.data)
+          changeLanguage(message.data.lang)
+        }
+      } catch (error) {
+        console.error('[content] Error in global runtime.onMessage listener:', error, message)
+      }
+    })
+
+    await overwriteAccessToken()
+    const isChatGptHost = location.hostname === 'chatgpt.com'
+    if (isChatGptHost) {
+      await manageChatGptTabState()
+
+      Browser.storage.onChanged.addListener(async (changes, areaName) => {
+        console.debug('[content] Storage changed:', changes, 'in area:', areaName)
+        try {
+          if (areaName === 'local' && (changes.userConfig || changes.config)) {
+            console.log(
+              '[content] User config changed in storage, re-evaluating ChatGPT tab state.',
+            )
+            await manageChatGptTabState()
+          }
+        } catch (error) {
+          console.error('[content] Error in storage.onChanged listener:', error)
+        }
       })
     }
 
-    const div = document.createElement('div')
-    document.body.append(div)
-    render(
-      ,
-      div,
-    )
+    await prepareForSelectionTools()
+    await prepareForSelectionToolsTouch()
+    await prepareForStaticCard()
+    await prepareForRightClickMenu()
+    await prepareForJumpBackNotification()
+
+    console.log('[content] Script run completed successfully.')
+  } catch (error) {
+    console.error('[content] Error in run function:', error)
   }
 }
 
-async function run() {
-  await getPreferredLanguageKey().then((lang) => {
-    changeLanguage(lang)
-  })
-  Browser.runtime.onMessage.addListener(async (message) => {
-    if (message.type === 'CHANGE_LANG') {
-      const data = message.data
-      changeLanguage(data.lang)
+let manageChatGptTabStatePromise = null
+
+async function manageChatGptTabState() {
+  if (manageChatGptTabStatePromise) {
+    console.debug('[content] manageChatGptTabState already running, waiting for in-flight call.')
+    return manageChatGptTabStatePromise
+  }
+
+  manageChatGptTabStatePromise = (async () => {
+    console.debug('[content] manageChatGptTabState called. Current location:', location.href)
+    try {
+      if (location.hostname !== 'chatgpt.com' || location.pathname === '/auth/login') {
+        console.debug(
+          '[content] Not on main chatgpt.com page, skipping manageChatGptTabState logic.',
+        )
+        return
+      }
+
+      const userConfig = await getUserConfig()
+      console.debug('[content] User config in manageChatGptTabState:', userConfig)
+      const isThisTabDesignatedForChatGptWeb = chatgptWebModelKeys.some((model) =>
+        getApiModesStringArrayFromConfig(userConfig, true).includes(model),
+      )
+      console.debug(
+        '[content] Is this tab designated for ChatGPT Web:',
+        isThisTabDesignatedForChatGptWeb,
+      )
+
+      if (isThisTabDesignatedForChatGptWeb) {
+        if (location.pathname === '/') {
+          console.debug('[content] On chatgpt.com root path.')
+          const input = document.querySelector('#prompt-textarea')
+          if (input && input.value === '') {
+            console.log('[content] Manipulating #prompt-textarea for focus.')
+            input.value = ' '
+            input.dispatchEvent(new Event('input', { bubbles: true }))
+            setTimeout(() => {
+              if (input && input.value === ' ') {
+                input.value = ''
+                input.dispatchEvent(new Event('input', { bubbles: true }))
+                console.debug('[content] #prompt-textarea manipulation complete.')
+              } else if (!input) {
+                console.warn(
+                  '[content] #prompt-textarea no longer available in setTimeout callback.',
+                )
+              }
+            }, 300)
+          } else {
+            console.debug(
+              '[content] #prompt-textarea not found, not empty (value: "' +
+                input?.value +
+                '"), or not on root path for manipulation.',
+            )
+          }
+        }
+
+        console.log('[content] Sending SET_CHATGPT_TAB message.')
+        await Browser.runtime.sendMessage({
+          type: 'SET_CHATGPT_TAB',
+          data: {},
+        })
+        console.log('[content] SET_CHATGPT_TAB message sent successfully.')
+      } else {
+        console.log('[content] This tab is NOT configured for ChatGPT Web model processing.')
+      }
+    } catch (error) {
+      console.error('[content] Error in manageChatGptTabState:', error)
     }
-  })
+  })()
 
-  await overwriteAccessToken()
-  await prepareForForegroundRequests()
+  try {
+    await manageChatGptTabStatePromise
+  } finally {
+    manageChatGptTabStatePromise = null
+  }
+}
 
-  prepareForSelectionTools()
-  prepareForSelectionToolsTouch()
-  prepareForStaticCard()
-  prepareForRightClickMenu()
-  prepareForJumpBackNotification()
+if (!window.__chatGPTBoxPortListenerRegistered) {
+  try {
+    if (location.hostname === 'chatgpt.com' && location.pathname !== '/auth/login') {
+      console.log('[content] Attempting to register port listener for chatgpt.com.')
+      registerPortListener(async (session, port) => {
+        console.debug(
+          `[content] Port listener callback triggered. Session:`,
+          session,
+          `Port:`,
+          port.name,
+        )
+        try {
+          if (isUsingChatgptWebModel(session)) {
+            console.log(
+              '[content] Session is for ChatGPT Web Model, processing request for question:',
+              session.question,
+            )
+            const accessToken = await getChatGptAccessToken()
+            if (!accessToken) {
+              console.warn('[content] No ChatGPT access token available for web API call.')
+              port.postMessage({ error: 'Missing ChatGPT access token.' })
+              return
+            }
+            await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken)
+            console.log('[content] generateAnswersWithChatgptWebApi call completed.')
+          } else {
+            console.debug(
+              '[content] Session is not for ChatGPT Web Model, skipping processing in this listener.',
+            )
+          }
+        } catch (e) {
+          console.error('[content] Error in port listener callback:', e, 'Session:', session)
+          try {
+            port.postMessage({
+              error: e.message || 'An unexpected error occurred in content script port listener.',
+            })
+          } catch (postError) {
+            console.error('[content] Error sending error message back via port:', postError)
+          }
+        }
+      })
+      console.log('[content] Generic port listener registered successfully for chatgpt.com pages.')
+      window.__chatGPTBoxPortListenerRegistered = true
+    } else {
+      console.debug(
+        '[content] Not on chatgpt.com or on login page, skipping port listener registration.',
+      )
+    }
+  } catch (error) {
+    console.error('[content] Error registering global port listener:', error)
+  }
+} else {
+  console.log('[content] Port listener already registered, skipping.')
 }
 
 run()