diff --git a/api-relay-server/package-lock.json b/api-relay-server/package-lock.json index b0b686f..1e6d133 100644 --- a/api-relay-server/package-lock.json +++ b/api-relay-server/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "api-relay-server": "file:", "body-parser": "^2.2.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -195,6 +196,10 @@ "node": ">= 8" } }, + "node_modules/api-relay-server": { + "resolved": "", + "link": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", diff --git a/api-relay-server/package.json b/api-relay-server/package.json index af7649c..93d6306 100644 --- a/api-relay-server/package.json +++ b/api-relay-server/package.json @@ -14,6 +14,7 @@ "license": "ISC", "description": "", "dependencies": { + "api-relay-server": "file:", "body-parser": "^2.2.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/api-relay-server/src/admin-ui/admin.html b/api-relay-server/src/admin-ui/admin.html index 91a440c..23d25f9 100644 --- a/api-relay-server/src/admin-ui/admin.html +++ b/api-relay-server/src/admin-ui/admin.html @@ -1,41 +1,102 @@ <!-- Chat Relay: Relay for AI Chat Interfaces Copyright (C) 2025 Jamison Moore - This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/. --> <!DOCTYPE html> <html lang="en"> + <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Chat Relay Admin</title> <style> - body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f4f4f4; color: #333; } - header { background-color: #333; color: #fff; padding: 1em; text-align: center; } - nav { background-color: #444; padding: 0.5em; } - nav ul { list-style-type: none; padding: 0; margin: 0; text-align: center; } - nav ul li { display: inline; margin-right: 20px; } - nav ul li a { color: #fff; text-decoration: none; font-weight: bold; } - nav ul li a.active { text-decoration: underline; } - .container { padding: 1em; } - .tab-content { display: none; } - .tab-content.active { display: block; } - h2 { border-bottom: 2px solid #333; padding-bottom: 0.5em; } - table { width: 100%; border-collapse: collapse; margin-top: 1em; } - th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } - th { background-color: #555; color: white; } + body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f4f4f4; + color: #333; + } + + header { + background-color: #333; + color: #fff; + padding: 1em; + text-align: center; + } + + nav { + background-color: #444; + padding: 0.5em; + } + + nav ul { + list-style-type: none; + padding: 0; + margin: 0; + text-align: center; + } + + nav ul li { + display: inline; + margin-right: 20px; + } + + nav ul li a { + color: #fff; + text-decoration: none; + font-weight: bold; + } + + nav ul li a.active { + text-decoration: underline; + } + + .container { + padding: 1em; + } + + .tab-content { + display: none; + } + + .tab-content.active { + display: block; + } + + h2 { + border-bottom: 2px solid #333; + padding-bottom: 0.5em; + } + + table { + width: 100%; + border-collapse: collapse; + margin-top: 1em; + } + + th, + td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; + } + + th { + background-color: #555; + color: white; + } + .log-window { background-color: #222; color: #0f0; @@ -46,10 +107,21 @@ border: 1px solid #444; margin-top: 1em; } - .log-entry { white-space: pre-wrap; } - .collapsible-header { background-color: #555; color: white; padding: 0.5em; cursor: pointer; text-align: center; } + + .log-entry { + white-space: pre-wrap; + } + + .collapsible-header { + background-color: #555; + color: white; + padding: 0.5em; + cursor: pointer; + text-align: center; + } </style> </head> + <body> <header> <h1>Chat Relay Admin Dashboard</h1> @@ -63,7 +135,8 @@ <h1>Chat Relay Admin Dashboard</h1> </nav> <div class="container"> <div id="messages" class="tab-content active"> - <h2>Message History <button id="refresh-messages-btn" style="font-size: 0.8em; margin-left: 10px;">Refresh Messages</button></h2> + <h2>Message History <button id="refresh-messages-btn" style="font-size: 0.8em; margin-left: 10px;">Refresh + Messages</button></h2> <table> <thead> <tr> @@ -97,7 +170,8 @@ <h2>Configuration Settings</h2> <div style="margin-top: 0.5em;"> <label>New Request Behavior (if extension busy):</label> <div> - <input type="radio" id="newRequestBehaviorQueue" name="newRequestBehavior" value="queue" checked> + <input type="radio" id="newRequestBehaviorQueue" name="newRequestBehavior" value="queue" + checked> <label for="newRequestBehaviorQueue">Queue</label> </div> <div> @@ -105,6 +179,10 @@ <h2>Configuration Settings</h2> <label for="newRequestBehaviorDrop">Drop</label> </div> </div> + <div style="margin-top: 0.5em;"> + <label for="auto-kill-port-input">Auto-kill conflicting port 3003 process on startup: </label> + <input type="checkbox" id="auto-kill-port-input"> + </div> <button id="save-settings-btn" style="margin-top: 1em; margin-bottom: 0.5em;">Save Settings</button> <span id="update-status-msg" style="margin-left: 10px; font-style: italic;"></span> <p style="margin-top: 1em;">Ping Interval (ms): <span id="setting-ping-interval"></span></p> @@ -115,7 +193,9 @@ <h2>Server Status</h2> <div id="status-content"> <p>Server Uptime: <span id="status-uptime">N/A</span></p> <p>Connected Extensions: <span id="status-connected-extensions">0</span></p> - <button id="restart-server-btn" style="margin-top: 1em; padding: 0.5em 1em; background-color: #d9534f; color: white; border: none; cursor: pointer;">Restart Server</button> + <button id="restart-server-btn" + style="margin-top: 1em; padding: 0.5em 1em; background-color: #d9534f; color: white; border: none; cursor: pointer;">Restart + Server</button> </div> </div> </div> @@ -135,7 +215,6 @@ <h2>Server Status</h2> link.classList.add('active'); const activeTabContent = document.getElementById(link.dataset.tab); activeTabContent.classList.add('active'); - // If settings or status tab is activated, refresh their content if (link.dataset.tab === 'settings' || link.dataset.tab === 'status') { fetchAndDisplayServerInfo(); @@ -152,7 +231,6 @@ <h2>Server Status</h2> } const messageHistoryBody = document.getElementById('message-history-body'); const refreshButton = document.getElementById('refresh-messages-btn'); - function createPreCell(data) { const cell = document.createElement('td'); if (data === undefined || data === null) { @@ -168,7 +246,6 @@ <h2>Server Status</h2> } return cell; } - async function fetchAndDisplayMessageHistory() { try { const response = await fetch('/v1/admin/message-history'); @@ -176,9 +253,7 @@ <h2>Server Status</h2> throw new Error(`HTTP error! status: ${response.status}`); } const messages = await response.json(); - messageHistoryBody.innerHTML = ''; // Clear existing rows - if (messages.length === 0) { const row = messageHistoryBody.insertRow(); const cell = row.insertCell(); @@ -187,7 +262,6 @@ <h2>Server Status</h2> cell.style.textAlign = 'center'; return; } - // Group messages by requestId const groupedMessages = messages.reduce((acc, logEntry) => { const id = logEntry.requestId; @@ -203,7 +277,6 @@ <h2>Server Status</h2> status: "Unknown" }; } - // Update fields based on log type switch (logEntry.type) { case 'CHAT_REQUEST_RECEIVED': @@ -231,22 +304,19 @@ <h2>Server Status</h2> } return acc; }, {}); - // Convert grouped messages object to an array and sort by timestamp (most recent first) const consolidatedMessages = Object.values(groupedMessages).sort((a, b) => { // Sort by startTimestamp, most recent first return new Date(b.startTimestamp) - new Date(a.startTimestamp); }); - if (consolidatedMessages.length === 0) { - const row = messageHistoryBody.insertRow(); + const row = messageHistoryBody.insertRow(); const cell = row.insertCell(); cell.colSpan = 8; // Adjusted for new column cell.textContent = 'No consolidated message history to display.'; cell.style.textAlign = 'center'; return; } - consolidatedMessages.forEach(msg => { const row = messageHistoryBody.insertRow(); row.insertCell().textContent = new Date(msg.startTimestamp).toLocaleString(); @@ -269,18 +339,16 @@ <h2>Server Status</h2> cell.style.textAlign = 'center'; } } - if (refreshButton) { refreshButton.addEventListener('click', fetchAndDisplayMessageHistory); } - fetchAndDisplayMessageHistory(); // Initial load for messages - // Elements for settings and status const portInputEl = document.getElementById('port-input'); // Corrected ID const requestTimeoutInputEl = document.getElementById('request-timeout-input'); const newRequestBehaviorQueueEl = document.getElementById('newRequestBehaviorQueue'); const newRequestBehaviorDropEl = document.getElementById('newRequestBehaviorDrop'); + const autoKillPortInputEl = document.getElementById('auto-kill-port-input'); const saveSettingsBtn = document.getElementById('save-settings-btn'); const updateStatusMsgEl = document.getElementById('update-status-msg'); const settingPingIntervalEl = document.getElementById('setting-ping-interval'); @@ -288,7 +356,6 @@ <h2>Server Status</h2> const statusUptimeEl = document.getElementById('status-uptime'); const statusConnectedExtensionsEl = document.getElementById('status-connected-extensions'); const restartServerBtn = document.getElementById('restart-server-btn'); - function formatUptime(totalSeconds) { if (totalSeconds === null || totalSeconds === undefined) return 'N/A'; const days = Math.floor(totalSeconds / (3600 * 24)); @@ -297,7 +364,6 @@ <h2>Server Status</h2> totalSeconds %= 3600; const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; - let uptimeString = ''; if (days > 0) uptimeString += `${days}d `; if (hours > 0) uptimeString += `${hours}h `; @@ -305,7 +371,6 @@ <h2>Server Status</h2> uptimeString += `${seconds}s`; return uptimeString.trim() || '0s'; } - async function fetchAndDisplayServerInfo() { try { const response = await fetch('/v1/admin/server-info'); @@ -313,44 +378,40 @@ <h2>Server Status</h2> throw new Error(`HTTP error! status: ${response.status}`); } const serverInfo = await response.json(); - // Populate Settings - if(portInputEl) portInputEl.value = serverInfo.port || ''; // Use portInputEl - if(requestTimeoutInputEl) requestTimeoutInputEl.value = serverInfo.requestTimeoutMs !== null ? serverInfo.requestTimeoutMs : ''; - if(settingPingIntervalEl) settingPingIntervalEl.textContent = serverInfo.pingIntervalMs !== null ? `${serverInfo.pingIntervalMs} ms` : 'N/A (Not Implemented)'; - + if (portInputEl) portInputEl.value = serverInfo.port || ''; // Use portInputEl + if (requestTimeoutInputEl) requestTimeoutInputEl.value = serverInfo.requestTimeoutMs !== null ? serverInfo.requestTimeoutMs : ''; + if (settingPingIntervalEl) settingPingIntervalEl.textContent = serverInfo.pingIntervalMs !== null ? `${serverInfo.pingIntervalMs} ms` : 'N/A (Not Implemented)'; if (serverInfo.newRequestBehavior === 'drop') { - if(newRequestBehaviorDropEl) newRequestBehaviorDropEl.checked = true; + if (newRequestBehaviorDropEl) newRequestBehaviorDropEl.checked = true; } else { - if(newRequestBehaviorQueueEl) newRequestBehaviorQueueEl.checked = true; // Default to queue + if (newRequestBehaviorQueueEl) newRequestBehaviorQueueEl.checked = true; // Default to queue } - + if (autoKillPortInputEl) autoKillPortInputEl.checked = serverInfo.autoKillPort || false; // Populate Status - if(statusUptimeEl) statusUptimeEl.textContent = formatUptime(serverInfo.uptimeSeconds); - if(statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = serverInfo.connectedExtensionsCount !== null ? serverInfo.connectedExtensionsCount : 'N/A'; - + if (statusUptimeEl) statusUptimeEl.textContent = formatUptime(serverInfo.uptimeSeconds); + if (statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = serverInfo.connectedExtensionsCount !== null ? serverInfo.connectedExtensionsCount : 'N/A'; } catch (error) { console.error('Error fetching server info:', error); - if(portInputEl) portInputEl.value = 'Error'; - if(requestTimeoutInputEl) requestTimeoutInputEl.value = 'Error'; - if(settingPingIntervalEl) settingPingIntervalEl.textContent = 'Error'; - if(newRequestBehaviorQueueEl) newRequestBehaviorQueueEl.checked = true; // Default on error + if (portInputEl) portInputEl.value = 'Error'; + if (requestTimeoutInputEl) requestTimeoutInputEl.value = 'Error'; + if (settingPingIntervalEl) settingPingIntervalEl.textContent = 'Error'; + if (newRequestBehaviorQueueEl) newRequestBehaviorQueueEl.checked = true; // Default on error + if (autoKillPortInputEl) autoKillPortInputEl.checked = false; // Default on error // Ensure error handling for status elements - if(statusUptimeEl) statusUptimeEl.textContent = 'Error loading uptime'; - if(statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = 'Error loading connections'; + if (statusUptimeEl) statusUptimeEl.textContent = 'Error loading uptime'; + if (statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = 'Error loading connections'; } } - async function handleSaveSettings() { - if (!requestTimeoutInputEl || !portInputEl || !updateStatusMsgEl || !newRequestBehaviorQueueEl || !newRequestBehaviorDropEl) return; - + if (!requestTimeoutInputEl || !portInputEl || !updateStatusMsgEl || !newRequestBehaviorQueueEl || !newRequestBehaviorDropEl || !autoKillPortInputEl) return; const newTimeout = parseInt(requestTimeoutInputEl.value, 10); const newPort = parseInt(portInputEl.value, 10); const selectedNewRequestBehavior = newRequestBehaviorQueueEl.checked ? 'queue' : 'drop'; + const autoKillPort = autoKillPortInputEl.checked; let settingsToUpdate = {}; let validationError = false; let messages = []; - if (requestTimeoutInputEl.value.trim() !== '') { // Only process if there's input if (!isNaN(newTimeout) && newTimeout > 0) { settingsToUpdate.requestTimeoutMs = newTimeout; @@ -359,37 +420,32 @@ <h2>Server Status</h2> validationError = true; } } - if (portInputEl.value.trim() !== '') { // Only process if there's input - if (!isNaN(newPort) && newPort > 0 && newPort <= 65535) { + if (!isNaN(newPort) && newPort > 0 && newPort <= 65535) { settingsToUpdate.port = newPort; } else { messages.push('Invalid port: Must be between 1 and 65535.'); validationError = true; } } - // Always include newRequestBehavior as it's controlled by radio buttons // No specific validation needed here as it's either 'queue' or 'drop' settingsToUpdate.newRequestBehavior = selectedNewRequestBehavior; - + settingsToUpdate.autoKillPort = autoKillPort; // Add the new setting if (validationError) { updateStatusMsgEl.textContent = messages.join(' '); updateStatusMsgEl.style.color = 'red'; setTimeout(() => { updateStatusMsgEl.textContent = ''; }, 7000); return; } - if (Object.keys(settingsToUpdate).length === 0) { updateStatusMsgEl.textContent = 'No changes to save.'; updateStatusMsgEl.style.color = 'blue'; setTimeout(() => { updateStatusMsgEl.textContent = ''; }, 5000); return; } - updateStatusMsgEl.textContent = 'Saving settings...'; updateStatusMsgEl.style.color = 'orange'; - try { const response = await fetch('/v1/admin/update-settings', { method: 'POST', @@ -415,11 +471,9 @@ <h2>Server Status</h2> const clearTime = updateStatusMsgEl.textContent.toLowerCase().includes('restart') ? 15000 : 7000; setTimeout(() => { updateStatusMsgEl.textContent = ''; }, clearTime); } - if (saveSettingsBtn) { saveSettingsBtn.addEventListener('click', handleSaveSettings); } - async function handleRestartServer() { if (confirm('Are you sure you want to restart the server?')) { try { @@ -432,15 +486,13 @@ <h2>Server Status</h2> } } } - if (restartServerBtn) { restartServerBtn.addEventListener('click', handleRestartServer); } - // Initial load for settings and status fetchAndDisplayServerInfo(); - console.log("Admin UI initialized. Message history, settings, status, and restart functionality implemented."); </script> </body> + </html> \ No newline at end of file diff --git a/api-relay-server/src/server.ts b/api-relay-server/src/server.ts index fe5654f..5cebc2c 100644 --- a/api-relay-server/src/server.ts +++ b/api-relay-server/src/server.ts @@ -15,13 +15,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ -import express, { Request, Response, NextFunction, Router } from 'express'; import bodyParser from 'body-parser'; +import { execSync } from 'child_process'; // Import for executing commands import cors from 'cors'; -import { WebSocketServer, WebSocket } from 'ws'; +import express, { NextFunction, Request, Response, Router } from 'express'; +import fs from 'fs'; import http from 'http'; import path from 'path'; -import fs from 'fs'; +import { WebSocket, WebSocketServer } from 'ws'; // Interfaces interface PendingRequest { resolve: (value: any) => void; @@ -91,16 +92,16 @@ type AdminLogDataType = ChatRequestData | ChatResponseData | ChatErrorData | any interface AdminLogEntry { timestamp: string; type: - | 'CHAT_REQUEST_RECEIVED' - | 'CHAT_RESPONSE_SENT' - | 'CHAT_ERROR_RESPONSE_SENT' - | 'CHAT_REQUEST_QUEUED' - | 'CHAT_REQUEST_DROPPED' - | 'CHAT_REQUEST_DEQUEUED' - | 'CHAT_REQUEST_PROCESSING' - | 'CHAT_REQUEST_ERROR' // For pre-processing errors like no extension - | 'SETTING_UPDATE' // Existing type, ensure it's included - | string; // Fallback for other/future types + | 'CHAT_REQUEST_RECEIVED' + | 'CHAT_RESPONSE_SENT' + | 'CHAT_ERROR_RESPONSE_SENT' + | 'CHAT_REQUEST_QUEUED' + | 'CHAT_REQUEST_DROPPED' + | 'CHAT_REQUEST_DEQUEUED' + | 'CHAT_REQUEST_PROCESSING' + | 'CHAT_REQUEST_ERROR' // For pre-processing errors like no extension + | 'SETTING_UPDATE' // Existing type, ensure it's included + | string; // Fallback for other/future types requestId: string; data: AdminLogDataType; } @@ -117,6 +118,7 @@ interface ServerConfig { requestTimeoutMs?: number; lastRestartRequestTimestamp?: number; // New field newRequestBehavior?: 'queue' | 'drop'; + autoKillPort?: boolean; // New setting for auto-killing port } // Function to read configuration @@ -147,8 +149,9 @@ const initialConfig = loadServerConfig(); // Initialize newRequestBehavior from config, defaulting to 'queue' newRequestBehavior = initialConfig.newRequestBehavior && (initialConfig.newRequestBehavior === 'queue' || initialConfig.newRequestBehavior === 'drop') - ? initialConfig.newRequestBehavior - : 'queue'; + ? initialConfig.newRequestBehavior + : 'queue'; +let autoKillPort = initialConfig.autoKillPort === undefined ? false : initialConfig.autoKillPort; // Initialize autoKillPort const PORT = initialConfig.port || parseInt(process.env.PORT || '3003', 10); let currentRequestTimeoutMs = initialConfig.requestTimeoutMs || parseInt(process.env.REQUEST_TIMEOUT_MS || '120000', 10); @@ -213,7 +216,7 @@ wss.on('connection', (ws: WebSocket) => { console.error(`SERVER: Rejecting request ${requestIdToProcess} with error: ${responseDataToUse}`); pendingRequest.reject(new Error(responseDataToUse || "Error from extension")); } else { - console.log(`SERVER: Resolving request ${requestIdToProcess} with data (first 100 chars): ${(responseDataToUse || "").substring(0,100)}`); + console.log(`SERVER: Resolving request ${requestIdToProcess} with data (first 100 chars): ${(responseDataToUse || "").substring(0, 100)}`); pendingRequest.resolve(responseDataToUse); } pendingRequests.delete(requestIdToProcess); @@ -242,7 +245,7 @@ async function logAdminMessage( data: AdminLogDataType // Use the specific union type for data ): Promise<void> { const timestamp = new Date().toISOString(); - + // For debugging, let's log what's being passed to logAdminMessage // console.log(`LOGGING [${type}] ReqID [${requestId}]:`, JSON.stringify(data, null, 2)); @@ -252,7 +255,7 @@ async function logAdminMessage( requestId: String(requestId), data, }; - + adminMessageHistory.unshift(logEntry); if (adminMessageHistory.length > MAX_ADMIN_HISTORY_LENGTH) { @@ -455,7 +458,7 @@ apiRouter.post('/chat/completions', async (req: Request, res: Response): Promise } return; } - + if (newRequestBehavior === 'queue') { requestQueue.push(queuedItem); logAdminMessage('CHAT_REQUEST_QUEUED', requestId, { @@ -476,8 +479,8 @@ apiRouter.post('/chat/completions', async (req: Request, res: Response): Promise // This catch is a safety net if processRequest itself throws an unhandled error *before* it can send a response. console.error(`SERVER: Unhandled error from processRequest for ${requestId} in /chat/completions:`, error); logAdminMessage('CHAT_ERROR_RESPONSE_SENT', requestId, { - toClientError: { message: (error as Error).message, type: "server_error", code: "unhandled_processing_catch" }, - status: `Error: ${(error as Error).message}` + toClientError: { message: (error as Error).message, type: "server_error", code: "unhandled_processing_catch" }, + status: `Error: ${(error as Error).message}` }).catch(err => console.error("ADMIN_LOG_ERROR (CHAT_ERROR_RESPONSE_SENT):", err)); if (!res.headersSent) { res.status(500).json({ @@ -525,13 +528,13 @@ apiRouter.get('/admin/message-history', (req: Request, res: Response): void => { } catch (error) { console.error('Error fetching message history from in-memory store:', error); if (!res.headersSent) { - res.status(500).json({ - error: { - message: (error instanceof Error ? error.message : String(error)) || 'Failed to retrieve message history', - type: 'server_error', // Changed from redis_error - code: 'history_retrieval_failed' - } - }); + res.status(500).json({ + error: { + message: (error instanceof Error ? error.message : String(error)) || 'Failed to retrieve message history', + type: 'server_error', // Changed from redis_error + code: 'history_retrieval_failed' + } + }); } } }); @@ -544,6 +547,7 @@ apiRouter.get('/admin/server-info', (req: Request, res: Response): void => { port: PORT, requestTimeoutMs: currentRequestTimeoutMs, // Report the current mutable value newRequestBehavior: newRequestBehavior, // Add the current behavior + autoKillPort: autoKillPort, // Add the current autoKillPort setting pingIntervalMs: null, // Placeholder - No explicit ping interval defined for client pings connectedExtensionsCount: activeConnections.length, uptimeSeconds: uptimeSeconds, @@ -552,13 +556,13 @@ apiRouter.get('/admin/server-info', (req: Request, res: Response): void => { } catch (error) { console.error('Error fetching server info:', error); if (!res.headersSent) { - res.status(500).json({ - error: { - message: (error instanceof Error ? error.message : String(error)) || 'Failed to retrieve server info', - type: 'server_error', - code: 'server_info_failed' - } - }); + res.status(500).json({ + error: { + message: (error instanceof Error ? error.message : String(error)) || 'Failed to retrieve server info', + type: 'server_error', + code: 'server_info_failed' + } + }); } } }); @@ -605,10 +609,10 @@ apiRouter.post('/admin/restart-server', (req: Request, res: Response): void => { // The more comprehensive update-settings endpoint below handles both port and requestTimeoutMs. apiRouter.post('/admin/update-settings', (req: Request, res: Response): void => { - const { requestTimeoutMs, port, newRequestBehavior: newBehaviorValue } = req.body; + const { requestTimeoutMs, port, newRequestBehavior: newBehaviorValue, autoKillPort: newAutoKillPortValue } = req.body; let configChanged = false; let messages: string[] = []; - + const currentConfig = loadServerConfig(); // Load current disk config to preserve other settings if (requestTimeoutMs !== undefined) { @@ -632,7 +636,7 @@ apiRouter.post('/admin/update-settings', (req: Request, res: Response): void => currentConfig.port = newPort; // Update config for saving configChanged = true; messages.push(`Server port configured to ${newPort}. This change will take effect after server restart.`); - logAdminMessage('SETTING_UPDATE', 'SERVER_CONFIG', { setting: 'port', value: newPort, requiresRestart: true }) + logAdminMessage('SETTING_UPDATE', 'SERVER_CONFIG', { setting: 'port', value: newPort, requiresRestart: true }) .catch(err => console.error("ADMIN_LOG_ERROR (SETTING_UPDATE):", err)); } else { res.status(400).json({ error: 'Invalid port value. Must be a positive number between 1 and 65535.' }); @@ -654,6 +658,20 @@ apiRouter.post('/admin/update-settings', (req: Request, res: Response): void => } } + if (newAutoKillPortValue !== undefined) { + if (typeof newAutoKillPortValue === 'boolean') { + autoKillPort = newAutoKillPortValue; // Update in-memory value immediately + currentConfig.autoKillPort = newAutoKillPortValue; // Update config for saving + configChanged = true; + messages.push(`Auto-kill port setting updated to '${autoKillPort}'. This change is effective on next server startup if port conflict occurs.`); + logAdminMessage('SETTING_UPDATE', 'SERVER_CONFIG', { setting: 'autoKillPort', value: autoKillPort, requiresRestartToSeeEffect: true }) + .catch(err => console.error("ADMIN_LOG_ERROR (SETTING_UPDATE autoKillPort):", err)); + } else { + res.status(400).json({ error: "Invalid autoKillPort value. Must be a boolean." }); + return; + } + } + if (configChanged) { saveServerConfig(currentConfig); res.json({ message: messages.join(' ') }); @@ -669,8 +687,73 @@ app.get('/health', (req: Request, res: Response) => { // Mount the API router app.use('/v1', apiRouter); + +// Function to handle port conflict before starting the server +function handlePortConflict(portToFree: number, killProcess: boolean): void { + if (!killProcess) { + console.log(`Auto-kill for port ${portToFree} is disabled. Will not attempt to free port.`); + return; + } + + console.log(`Checking if port ${portToFree} is in use...`); + try { + // Command to find process using the port (Windows specific) + const command = `netstat -ano -p TCP | findstr ":${portToFree}.*LISTENING"`; + const output = execSync(command, { encoding: 'utf-8' }); + + if (output) { + console.log(`Port ${portToFree} is in use. Output:\n${output}`); + // Extract PID - Example: TCP 0.0.0.0:3003 0.0.0.0:0 LISTENING 12345 + // PID is the last number on the line. + const lines = output.trim().split('\n'); + if (lines.length > 0) { + const firstLine = lines[0]; + const parts = firstLine.trim().split(/\s+/); + const pid = parts[parts.length - 1]; + + if (pid && !isNaN(parseInt(pid))) { + console.log(`Attempting to kill process with PID: ${pid} using port ${portToFree}`); + try { + execSync(`taskkill /PID ${pid} /F`); + console.log(`Successfully killed process ${pid} using port ${portToFree}.`); + logAdminMessage('PORT_KILLED', `PORT_${portToFree}`, { port: portToFree, pid: pid, status: 'success' }) + .catch(err => console.error("ADMIN_LOG_ERROR (PORT_KILLED):", err)); + } catch (killError) { + console.error(`Failed to kill process ${pid} using port ${portToFree}:`, killError); + logAdminMessage('PORT_KILL_FAILED', `PORT_${portToFree}`, { port: portToFree, pid: pid, status: 'failure', error: (killError as Error).message }) + .catch(err => console.error("ADMIN_LOG_ERROR (PORT_KILL_FAILED):", err)); + } + } else { + console.warn(`Could not extract a valid PID for port ${portToFree} from netstat output: ${firstLine}`); + } + } else { + console.log(`No process found listening on port ${portToFree} from netstat output.`); + } + } else { + console.log(`Port ${portToFree} is free.`); + } + } catch (error: any) { + // If findstr returns an error, it usually means the port is not found / not in use. + if (error.status === 1) { // findstr exits with 1 if string not found + console.log(`Port ${portToFree} appears to be free (netstat/findstr did not find it).`); + } else { + console.error(`Error checking port ${portToFree}:`, error.message); + } + } +} + // Start the server -server.listen(PORT, () => { - console.log(`OpenAI-compatible relay server running on port ${PORT}`); - console.log(`WebSocket server for browser extensions running on ws://localhost:${PORT}`); +async function startServer() { + // Handle potential port conflict before starting the server + handlePortConflict(PORT, autoKillPort); + + server.listen(PORT, () => { + console.log(`OpenAI-compatible relay server running on port ${PORT}`); + console.log(`WebSocket server for browser extensions running on ws://localhost:${PORT}`); + }); +} + +startServer().catch(err => { + console.error("Failed to start server:", err); + process.exit(1); }); diff --git a/extension/background.js b/extension/background.js index f392107..fdc947c 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1,27 +1,7 @@ -/* - * Chat Relay: Relay for AI Chat Interfaces - * Copyright (C) 2025 Jamison Moore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - */ -// AI Chat Relay - Background Script - -// Default settings const DEFAULT_SETTINGS = { - serverHost: 'localhost', - serverPort: 3003, - serverProtocol: 'ws' + serverHost: 'localhost', + serverPort: 3003, + serverProtocol: 'ws' }; let relaySocket = null; @@ -29,757 +9,783 @@ let reconnectInterval = 5000; let reconnectTimer = null; let activeTabId = null; let serverUrl = ''; -let lastRequestId = null; // User's global lastRequestId -let processingRequest = false; // User's global processing flag -let pendingRequests = []; // User's command queue -let lastSuccessfullyProcessedMessageText = null; // Text of the last message successfully processed (AI response or duplicate handled) -const pendingRequestDetails = new Map(); // Stores { text: string } for active requests, keyed by requestId +let lastRequestId = null; +let processingRequest = false; +let pendingRequests = []; +let lastSuccessfullyProcessedMessageText = null; +const pendingRequestDetails = new Map(); +let currentRequestTargetTabId = null; -// Supported domains for chat interfaces const supportedDomains = ['gemini.google.com', 'aistudio.google.com', 'chatgpt.com', 'claude.ai']; -// ===== DEBUGGER RELATED GLOBALS ===== const BG_LOG_PREFIX = '[BG DEBUGGER]'; -let debuggerAttachedTabs = new Map(); // tabId -> { providerName, patterns, isFetchEnabled, isAttached, lastKnownRequestId } +let debuggerAttachedTabs = new Map(); -// Load settings and connect to the relay server function loadSettingsAndConnect() { - console.log("BACKGROUND: Loading settings and connecting to relay server"); - chrome.storage.sync.get(DEFAULT_SETTINGS, (items) => { - serverUrl = `${items.serverProtocol}://${items.serverHost}:${items.serverPort}`; - console.log("BACKGROUND: Using server URL:", serverUrl); - connectToRelayServer(); - }); + console.log("BACKGROUND: Loading settings and connecting to relay server"); + chrome.storage.sync.get(DEFAULT_SETTINGS, (items) => { + serverUrl = `${items.serverProtocol}://${items.serverHost}:${items.serverPort}`; + console.log("BACKGROUND: Using server URL:", serverUrl); + connectToRelayServer(); + }); } -// Connect to the relay server function connectToRelayServer() { - if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { - console.log("BACKGROUND: Relay WS: Already connected."); - return; - } - - if (!navigator.onLine) { - console.warn("BACKGROUND: Network offline. Deferring connection attempt."); - if (reconnectTimer) clearTimeout(reconnectTimer); - reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval); - return; - } - - const healthCheckUrl = serverUrl.replace(/^ws/, 'http') + '/health'; - console.log("BACKGROUND: Performing HTTP pre-check to", healthCheckUrl); - - fetch(healthCheckUrl) - .then(response => { - if (!response.ok) { - // Server responded, but not with a 2xx status (e.g., 404, 500) - console.warn(`BACKGROUND: HTTP pre-check to ${healthCheckUrl} received non-OK status: ${response.status}. Server might be having issues. Deferring WebSocket attempt.`); - return Promise.reject(new Error(`Server responded with ${response.status}`)); - } - return response.json(); // Attempt to parse JSON - }) - .then(healthData => { - console.log(`BACKGROUND: HTTP pre-check to ${healthCheckUrl} successful. Server status: ${healthData.status}, Active Connections: ${healthData.activeBrowserConnections}. Proceeding with WebSocket connection.`); - attemptWebSocketConnection(); - }) - .catch(fetchError => { - // This catches network errors (server down) or errors from the .then() chain (non-OK response, JSON parse error) - console.warn(`BACKGROUND: HTTP pre-check to ${healthCheckUrl} failed: ${fetchError.message}. Server is likely down, unreachable, or health endpoint is misbehaving. Deferring WebSocket attempt.`); - relaySocket = null; - if (reconnectTimer) clearTimeout(reconnectTimer); - reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval); - }); + if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { + console.log("BACKGROUND: Relay WS: Already connected."); + return; + } + + if (!navigator.onLine) { + console.warn("BACKGROUND: Network offline. Deferring connection attempt."); + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval); + return; + } + + const healthCheckUrl = serverUrl.replace(/^ws/, 'http') + '/health'; + console.log("BACKGROUND: Performing HTTP pre-check to", healthCheckUrl); + + fetch(healthCheckUrl) + .then(response => { + if (!response.ok) { + console.warn(`BACKGROUND: HTTP pre-check to ${healthCheckUrl} received non-OK status: ${response.status}. Server might be having issues. Deferring WebSocket attempt.`); + return Promise.reject(new Error(`Server responded with ${response.status}`)); + } + return response.json(); + }) + .then(healthData => { + console.log(`BACKGROUND: HTTP pre-check to ${healthCheckUrl} successful. Server status: ${healthData.status}, Active Connections: ${healthData.activeBrowserConnections}. Proceeding with WebSocket connection.`); + attemptWebSocketConnection(); + }) + .catch(fetchError => { + console.warn(`BACKGROUND: HTTP pre-check to ${healthCheckUrl} failed: ${fetchError.message}. Server is likely down, unreachable, or health endpoint is misbehaving. Deferring WebSocket attempt.`); + relaySocket = null; + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval); + }); } function attemptWebSocketConnection() { - console.log("BACKGROUND: Relay WS: Attempting to connect to", serverUrl); - try { - relaySocket = new WebSocket(serverUrl); - - relaySocket.onopen = () => { - console.log("BACKGROUND: Relay WS: Connection established with relay server."); - reconnectInterval = 5000; // Reset reconnect interval on successful connection - if (reconnectTimer) clearTimeout(reconnectTimer); - reconnectTimer = null; - }; - - relaySocket.onmessage = (event) => { - console.log("BACKGROUND: Relay WS: Message received from relay server:", event.data); - try { - const command = JSON.parse(event.data); - if (command.type === 'SEND_CHAT_MESSAGE') { - console.log("BACKGROUND: Received SEND_CHAT_MESSAGE command with requestId:", command.requestId); - - // Store details for this new request - pendingRequestDetails.set(command.requestId, { messageContent: command.message }); // Changed key 'text' to 'messageContent' - let messagePreview = ""; - const messageValue = command.message; - if (typeof messageValue === 'string') { - messagePreview = `String: "${messageValue.substring(0, 50)}..."`; - } else if (messageValue instanceof ArrayBuffer) { - messagePreview = `ArrayBuffer data (size: ${messageValue.byteLength} bytes)`; - } else if (messageValue instanceof Blob) { - messagePreview = `Blob data (size: ${messageValue.size} bytes, type: ${messageValue.type})`; - } else if (messageValue && typeof messageValue === 'object' && messageValue !== null) { - messagePreview = `Object data (type: ${Object.prototype.toString.call(messageValue)})`; - } else { - messagePreview = `Data type: ${typeof messageValue}, Value: ${String(messageValue).substring(0,50)}`; - } - console.log(`BACKGROUND: Stored details for requestId: ${command.requestId}, message: ${messagePreview}`); - - // Add to the queue - pendingRequests.push(command); - console.log(`BACKGROUND: Added command with requestId: ${command.requestId} to queue. Queue length: ${pendingRequests.length}`); - - // Attempt to process the next request in the queue - processNextRequest(); - } - } catch (error) { - console.error("BACKGROUND: Relay WS: Error processing message from relay server:", error); - } - }; - - relaySocket.onerror = (errorEvent) => { - console.warn("BACKGROUND: Relay WS: WebSocket connection error (event):", errorEvent); - // onclose will typically follow and handle reconnection logic - }; - - relaySocket.onclose = (closeEvent) => { - console.log(`BACKGROUND: Relay WS: Connection closed (event). Code: ${closeEvent.code}, Reason: '${closeEvent.reason || 'N/A'}', Cleanly: ${closeEvent.wasClean}. Will attempt reconnect (via connectToRelayServer) in ${reconnectInterval / 1000}s.`); - relaySocket = null; - if (reconnectTimer) clearTimeout(reconnectTimer); - // Retry the entire connectToRelayServer process, which includes the HTTP pre-check - reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval); - }; - } catch (instantiationError) { - console.error("BACKGROUND: Relay WS: Error instantiating WebSocket:", instantiationError); - relaySocket = null; - if (reconnectTimer) clearTimeout(reconnectTimer); - console.log(`BACKGROUND: Relay WS: Instantiation failed. Will attempt reconnect (via connectToRelayServer) in ${reconnectInterval / 1000}s.`); - // Retry the entire connectToRelayServer process - reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval); - } + console.log("BACKGROUND: Relay WS: Attempting to connect to", serverUrl); + try { + relaySocket = new WebSocket(serverUrl); + + relaySocket.onopen = () => { + console.log("BACKGROUND: Relay WS: Connection established with relay server."); + reconnectInterval = 5000; + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = null; + }; + + relaySocket.onmessage = (event) => { + console.log("BACKGROUND: Relay WS: Message received from relay server:", event.data); + try { + const command = JSON.parse(event.data); + if (command.type === 'SEND_CHAT_MESSAGE') { + console.log("BACKGROUND: Received SEND_CHAT_MESSAGE command with requestId:", command.requestId); + + pendingRequestDetails.set(command.requestId, { + messageContent: command.message, + settings: command.settings + }); + let messagePreview = ""; + const messageValue = command.message; + if (typeof messageValue === 'string') { + messagePreview = `String: "${messageValue.substring(0, 50)}..."`; + } else if (messageValue instanceof ArrayBuffer) { + messagePreview = `ArrayBuffer data (size: ${messageValue.byteLength} bytes)`; + } else if (messageValue instanceof Blob) { + messagePreview = `Blob data (size: ${messageValue.size} bytes, type: ${messageValue.type})`; + } else if (messageValue && typeof messageValue === 'object' && messageValue !== null) { + messagePreview = `Object data (type: ${Object.prototype.toString.call(messageValue)})`; + } else { + messagePreview = `Data type: ${typeof messageValue}, Value: ${String(messageValue).substring(0, 50)}`; + } + console.log(`BACKGROUND: Stored details for requestId: ${command.requestId}, message: ${messagePreview}`); + + pendingRequests.push(command); + console.log(`BACKGROUND: Added command with requestId: ${command.requestId} to queue. Queue length: ${pendingRequests.length}`); + + processNextRequest(); + } + } catch (error) { + console.error("BACKGROUND: Relay WS: Error processing message from relay server:", error); + } + }; + + relaySocket.onerror = (errorEvent) => { + console.warn("BACKGROUND: Relay WS: WebSocket connection error (event):", errorEvent); + }; + + relaySocket.onclose = (closeEvent) => { + console.log(`BACKGROUND: Relay WS: Connection closed (event). Code: ${closeEvent.code}, Reason: '${closeEvent.reason || 'N/A'}', Cleanly: ${closeEvent.wasClean}. Will attempt reconnect (via connectToRelayServer) in ${reconnectInterval / 1000}s.`); + relaySocket = null; + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval); + }; + } catch (instantiationError) { + console.error("BACKGROUND: Relay WS: Error instantiating WebSocket:", instantiationError); + relaySocket = null; + if (reconnectTimer) clearTimeout(reconnectTimer); + console.log(`BACKGROUND: Relay WS: Instantiation failed. Will attempt reconnect (via connectToRelayServer) in ${reconnectInterval / 1000}s.`); + reconnectTimer = setTimeout(connectToRelayServer, reconnectInterval); + } } -// Forward commands to content script -async function forwardCommandToContentScript(command) { // command will include original requestId - try { - console.log("BACKGROUND: Forwarding command to content script:", command); - let targetTabIdForCommand = null; - - if (activeTabId) { - try { - console.log(`BACKGROUND: Attempting to use stored activeTabId: ${activeTabId}`); - // Test send to ensure tab is still valid for this command before associating requestId - await new Promise((resolve, reject) => { - chrome.tabs.sendMessage(activeTabId, { type: "PING_TAB" }, response => { // Ping before associating - if (chrome.runtime.lastError || !response || !response.success) { - console.warn(`BACKGROUND: Ping to stored tab ${activeTabId} failed or no ack:`, chrome.runtime.lastError ? chrome.runtime.lastError.message : "No response/success false"); - activeTabId = null; // Invalidate activeTabId - reject(new Error("Ping failed")); +async function forwardCommandToContentScript(command) { + try { + console.log("BACKGROUND: Forwarding command to content script:", command); + let targetTabIdForCommand = null; + + if (activeTabId) { + try { + console.log(`BACKGROUND: Attempting to use stored activeTabId: ${activeTabId}`); + await new Promise((resolve, reject) => { + chrome.tabs.sendMessage(activeTabId, { type: "PING_TAB" }, response => { + if (chrome.runtime.lastError || !response || !response.success) { + console.warn(`BACKGROUND: Ping to stored tab ${activeTabId} failed or no ack:`, chrome.runtime.lastError ? chrome.runtime.lastError.message : "No response/success false"); + activeTabId = null; + reject(new Error("Ping failed")); + } else { + console.log(`BACKGROUND: Ping to stored tab ${activeTabId} successful.`); + targetTabIdForCommand = activeTabId; + resolve(); + } + }); + }); + } catch (error) { + console.warn(`BACKGROUND: Error using stored activeTabId ${activeTabId}, will find new tab:`, error); + } + } + + if (!targetTabIdForCommand) { + targetTabIdForCommand = await findAndSendToSuitableTab(command, true); + } + + if (targetTabIdForCommand) { + if (processingRequest && command.requestId === lastRequestId) { + currentRequestTargetTabId = targetTabIdForCommand; + console.log(`BACKGROUND: Set currentRequestTargetTabId to ${targetTabIdForCommand} for active requestId ${lastRequestId}`); + } + const tabInfo = debuggerAttachedTabs.get(targetTabIdForCommand); + if (tabInfo) { + tabInfo.lastKnownRequestId = command.requestId; + console.log(BG_LOG_PREFIX, `Associated requestId ${command.requestId} with tab ${targetTabIdForCommand} for debugger.`); + } else { + console.warn(BG_LOG_PREFIX, `Tab ${targetTabIdForCommand} is not being debugged. Cannot associate requestId for debugger.`); + } + + chrome.tabs.sendMessage(targetTabIdForCommand, command, (response) => { + if (chrome.runtime.lastError) { + console.error(`BACKGROUND: Error sending message to tab ${targetTabIdForCommand}:`, chrome.runtime.lastError.message); + if (lastRequestId === command.requestId) { + processingRequest = false; + } } else { - console.log(`BACKGROUND: Ping to stored tab ${activeTabId} successful.`); - targetTabIdForCommand = activeTabId; - resolve(); + console.log(`BACKGROUND: Content script in tab ${targetTabIdForCommand} acknowledged command:`, response); } }); - }); - } catch (error) { - // Fall through to findAndSendToSuitableTab if ping fails - console.warn(`BACKGROUND: Error using stored activeTabId ${activeTabId}, will find new tab:`, error); - } - } - - if (!targetTabIdForCommand) { - targetTabIdForCommand = await findAndSendToSuitableTab(command, true); // Pass true to only find, not send yet - } - if (targetTabIdForCommand) { - const tabInfo = debuggerAttachedTabs.get(targetTabIdForCommand); - if (tabInfo) { - tabInfo.lastKnownRequestId = command.requestId; // Store command's requestId for this specific tab - console.log(BG_LOG_PREFIX, `Associated requestId ${command.requestId} with tab ${targetTabIdForCommand} for debugger.`); } else { - console.warn(BG_LOG_PREFIX, `Tab ${targetTabIdForCommand} is not being debugged. Cannot associate requestId for debugger.`); - } + const errorMsg = "Could not find any suitable tab for command."; + console.error(`BACKGROUND: ${errorMsg} for requestId: ${command.requestId}.`); + + if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE_ERROR", + requestId: command.requestId, + error: errorMsg + })); + console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR to server for requestId: ${command.requestId} (no suitable tab).`); + } else { + console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE_ERROR for requestId: ${command.requestId} (no suitable tab).`); + } - // Now actually send the command - chrome.tabs.sendMessage(targetTabIdForCommand, command, (response) => { - if (chrome.runtime.lastError) { - console.error(`BACKGROUND: Error sending message to tab ${targetTabIdForCommand}:`, chrome.runtime.lastError.message); - if (lastRequestId === command.requestId) { + if (lastRequestId === command.requestId) { processingRequest = false; + currentRequestTargetTabId = null; + console.log(`BACKGROUND: Reset processingRequest and currentRequestTargetTabId for requestId: ${command.requestId} (no suitable tab).`); } - } else { - console.log(`BACKGROUND: Content script in tab ${targetTabIdForCommand} acknowledged command:`, response); - } - }); + processNextRequest(); + } - } else { - const errorMsg = "Could not find any suitable tab for command."; - console.error(`BACKGROUND: ${errorMsg} for requestId: ${command.requestId}.`); - + } catch (error) { + console.error("BACKGROUND: Error in forwardCommandToContentScript for requestId:", command.requestId, error); if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { relaySocket.send(JSON.stringify({ type: "CHAT_RESPONSE_ERROR", requestId: command.requestId, - error: errorMsg + error: `Internal error in background script while forwarding command: ${error.message}` })); - console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR to server for requestId: ${command.requestId} (no suitable tab).`); + console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR to server for requestId: ${command.requestId} (exception).`); } else { - console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE_ERROR for requestId: ${command.requestId} (no suitable tab).`); + console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE_ERROR for requestId: ${command.requestId} (exception).`); } if (lastRequestId === command.requestId) { processingRequest = false; - console.log(`BACKGROUND: Reset processingRequest for requestId: ${command.requestId} (no suitable tab).`); + currentRequestTargetTabId = null; + console.log(`BACKGROUND: Reset processingRequest and currentRequestTargetTabId for requestId: ${command.requestId} (exception).`); } - // Ensure processNextRequest is called to handle any queued items, - // even if this one failed. - processNextRequest(); } - - } catch (error) { - console.error("BACKGROUND: Error in forwardCommandToContentScript for requestId:", command.requestId, error); - // Send an error back to the server if an unexpected error occurs during forwarding - if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { - relaySocket.send(JSON.stringify({ - type: "CHAT_RESPONSE_ERROR", - requestId: command.requestId, - error: `Internal error in background script while forwarding command: ${error.message}` - })); - console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR to server for requestId: ${command.requestId} (exception).`); - } else { - console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE_ERROR for requestId: ${command.requestId} (exception).`); - } - - if (lastRequestId === command.requestId) { - processingRequest = false; - console.log(`BACKGROUND: Reset processingRequest for requestId: ${command.requestId} (exception).`); - } - } } -// Helper function to find a suitable tab and send the command async function findAndSendToSuitableTab(command, justFinding = false) { - try { - console.log("BACKGROUND: Finding suitable tab for command:", command); - const allTabs = await chrome.tabs.query({}); - const matchingTabs = allTabs.filter(tab => { - if (!tab.url) return false; - return supportedDomains.some(domain => tab.url.includes(domain)); - }); - - console.log(`BACKGROUND: Found ${matchingTabs.length} tabs matching supported domains`); - - if (matchingTabs.length > 0) { - const activeMatchingTabs = matchingTabs.filter(tab => tab.active); - const targetTab = activeMatchingTabs.length > 0 ? activeMatchingTabs[0] : matchingTabs[0]; - console.log(`BACKGROUND: Selected tab ${targetTab.id} (${targetTab.url})`); - activeTabId = targetTab.id; // Update global activeTabId - - if (justFinding) { - return targetTab.id; - } - - console.warn("BACKGROUND: findAndSendToSuitableTab called with justFinding=false. Sending is now handled by caller."); - return targetTab.id; + try { + console.log("BACKGROUND: Finding suitable tab for command:", command); + const allTabs = await chrome.tabs.query({}); + const matchingTabs = allTabs.filter(tab => { + if (!tab.url) return false; + return supportedDomains.some(domain => tab.url.includes(domain)); + }); - } else { - console.error("BACKGROUND: Could not find any tabs matching supported domains."); - return null; + console.log(`BACKGROUND: Found ${matchingTabs.length} tabs matching supported domains`); + + if (matchingTabs.length > 0) { + const activeMatchingTabs = matchingTabs.filter(tab => tab.active); + const targetTab = activeMatchingTabs.length > 0 ? activeMatchingTabs[0] : matchingTabs[0]; + console.log(`BACKGROUND: Selected tab ${targetTab.id} (${targetTab.url})`); + activeTabId = targetTab.id; + + if (justFinding) { + return targetTab.id; + } + + console.warn("BACKGROUND: findAndSendToSuitableTab called with justFinding=false. Sending is now handled by caller."); + return targetTab.id; + + } else { + console.error("BACKGROUND: Could not find any tabs matching supported domains."); + return null; + } + } catch (error) { + console.error("BACKGROUND: Error finding suitable tab:", error); + return null; } - } catch (error) { - console.error("BACKGROUND: Error finding suitable tab:", error); - return null; - } } -// Process the next request in the queue function processNextRequest() { - console.log("BACKGROUND: Processing next request, queue length:", pendingRequests.length); - if (processingRequest && pendingRequests.length > 0) { - console.log("BACKGROUND: Still processing a request, deferring processNextRequest call."); - return; - } - - if (pendingRequests.length > 0) { - const nextCommand = pendingRequests.shift(); - console.log("BACKGROUND: Processing next command from queue:", nextCommand); - - // Ensure details are stored if this came from the pendingRequests queue - // (though ideally they are stored when initially received from server) - if (!pendingRequestDetails.has(nextCommand.requestId) && nextCommand.message !== undefined) { - pendingRequestDetails.set(nextCommand.requestId, { messageContent: nextCommand.message }); // Use messageContent - let preview = typeof nextCommand.message === 'string' ? `"${nextCommand.message.substring(0,30)}..."` : `Type: ${typeof nextCommand.message}`; - console.log(`BACKGROUND: Stored details (messageContent) for queued requestId: ${nextCommand.requestId} (Message: ${preview}) while processing queue.`); + console.log("BACKGROUND: Processing next request, queue length:", pendingRequests.length); + if (processingRequest && pendingRequests.length > 0) { + console.log("BACKGROUND: Still processing a request, deferring processNextRequest call."); + return; } - processingRequest = true; - lastRequestId = nextCommand.requestId; - - // Add a delay before forwarding the command - setTimeout(() => { - forwardCommandToContentScript({ - action: "SEND_CHAT_MESSAGE", - requestId: nextCommand.requestId, - messageContent: nextCommand.message, - settings: nextCommand.settings, - lastProcessedText: lastSuccessfullyProcessedMessageText // Pass the text of the last successfully processed message - }); - }, 500); // 500ms delay - } else { - console.log("BACKGROUND: No pending requests to process."); - } + if (pendingRequests.length > 0) { + const nextCommand = pendingRequests.shift(); + console.log("BACKGROUND: Processing next command from queue:", nextCommand); + + if (!pendingRequestDetails.has(nextCommand.requestId) && nextCommand.message !== undefined) { + pendingRequestDetails.set(nextCommand.requestId, { messageContent: nextCommand.message }); + let preview = typeof nextCommand.message === 'string' ? `"${nextCommand.message.substring(0, 30)}..."` : `Type: ${typeof nextCommand.message}`; + console.log(`BACKGROUND: Stored details (messageContent) for queued requestId: ${nextCommand.requestId} (Message: ${preview}) while processing queue.`); + } + + processingRequest = true; + lastRequestId = nextCommand.requestId; + + setTimeout(() => { + forwardCommandToContentScript({ + action: "SEND_CHAT_MESSAGE", + requestId: nextCommand.requestId, + messageContent: nextCommand.message, + settings: nextCommand.settings, + lastProcessedText: lastSuccessfullyProcessedMessageText + }); + }, 500); + } else { + console.log("BACKGROUND: No pending requests to process."); + } } -// Helper function to check if a URL is supported by a given provider -// This might need to be more sophisticated if provider domains are complex function isUrlSupportedByProvider(url, providerName) { - // This function would need access to the provider definitions or a shared config - // For AIStudioProvider: if (providerName === "AIStudioProvider") { return url.includes("aistudio.google.com"); } - // For GeminiProvider: if (providerName === "GeminiProvider") { return url.includes("gemini.google.com"); } - // For ChatGPTProvider: - if (providerName === "ChatGptProvider") { // Match the casing used by the provider's .name property + if (providerName === "ChatGptProvider") { return url.includes("chatgpt.com"); } - // For ClaudeProvider: - if (providerName === "ClaudeProvider") { // Match the casing used by the provider's .name property + if (providerName === "ClaudeProvider") { return url.includes("claude.ai"); } - // Add other providers if necessary console.warn(BG_LOG_PREFIX, `isUrlSupportedByProvider: Unknown providerName '${providerName}'`); return false; } -// Listen for tab updates chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { - if (changeInfo.status === 'complete' && tab.url) { - const isSupportedDomain = supportedDomains.some(domain => tab.url.includes(domain)); - if (isSupportedDomain) { - console.log(`BACKGROUND: A supported tab ${tabId} (${tab.url}) was updated. Checking if it should be the active tab.`); - // Potentially update activeTabId, but be careful if multiple supported tabs are open. - // The existing logic for activeTabId update via messages from content script might be more reliable. - // For now, let's ensure it's set if it's the *only* active one or becomes active. - if (tab.active || !activeTabId) { - // Check if this tab is actually one of the supported types before making it active - // This is a bit redundant with supportedDomains check but good for clarity - const currentProvider = providerUtils.getProviderForUrl(tab.url); // Assuming providerUtils is accessible or we have a similar utility - if (currentProvider) { - activeTabId = tabId; - console.log(`BACKGROUND: Set ${tabId} (${tab.url}) as the active tab.`); - } - } - } - } - - // Handle debugger re-attachment on URL changes for already debugged tabs - const attachmentDetails = debuggerAttachedTabs.get(tabId); - if (attachmentDetails && attachmentDetails.isAttached && changeInfo.url && tab && tab.url) { - // changeInfo.url is the old URL, tab.url is the new one - console.log(BG_LOG_PREFIX, `Tab ${tabId} updated. Old URL: ${changeInfo.url}, New URL: ${tab.url}. Checking debugger status.`); - - const providerStillValidForNewUrl = isUrlSupportedByProvider(tab.url, attachmentDetails.providerName); - - if (providerStillValidForNewUrl) { - console.log(BG_LOG_PREFIX, `Tab ${tabId} URL changed to ${tab.url}. Provider ${attachmentDetails.providerName} still valid. Re-initiating debugger attachment.`); - const oldProviderName = attachmentDetails.providerName; - const oldPatterns = attachmentDetails.patterns; // These patterns were from the content script for the *domain* - - // Detach first to ensure a clean state, then re-attach. - // The 'isAttached' flag in attachmentDetails will be set to false by detachDebugger. - await detachDebugger(tabId); - - // Check if tab still exists (it should, as we are in its onUpdated event) - try { - const updatedTabInfo = await chrome.tabs.get(tabId); - if (updatedTabInfo) { - console.log(BG_LOG_PREFIX, `Proactively re-attaching debugger to ${tabId} (${updatedTabInfo.url}) with provider ${oldProviderName}.`); - // Content script should send SET_DEBUGGER_TARGETS on its re-initialization. - // However, a proactive re-attachment can be beneficial. - // The patterns might need to be re-fetched if they are URL-specific beyond the domain. - // For now, using oldPatterns, assuming they are domain-level. - await attachDebuggerAndEnableFetch(tabId, oldProviderName, oldPatterns); + if (changeInfo.status === 'complete' && tab.url) { + const isSupportedDomain = supportedDomains.some(domain => tab.url.includes(domain)); + if (isSupportedDomain) { + console.log(`BACKGROUND: A supported tab ${tabId} (${tab.url}) was updated. Checking if it should be the active tab.`); + if (tab.active || !activeTabId) { + const currentProvider = providerUtils.getProviderForUrl(tab.url); + if (currentProvider) { + activeTabId = tabId; + console.log(`BACKGROUND: Set ${tabId} (${tab.url}) as the active tab.`); + } + } } - } catch (error) { - console.warn(BG_LOG_PREFIX, `Error getting tab info for ${tabId} during re-attachment attempt:`, error.message); - } - - } else { - console.log(BG_LOG_PREFIX, `Tab ${tabId} URL changed to ${tab.url}. Provider ${attachmentDetails.providerName} no longer valid or URL not supported by provider. Detaching debugger.`); - await detachDebugger(tabId); } - } else if (attachmentDetails && attachmentDetails.isAttached && changeInfo.status === 'loading' && tab && tab.url && !changeInfo.url) { - // Sometimes URL change is only visible when status is 'loading' and tab.url is the new one. - // This is a more aggressive check. - const newUrl = tab.url; - console.log(BG_LOG_PREFIX, `Tab ${tabId} is loading new URL: ${newUrl}. Checking debugger status.`); - const providerStillValidForNewUrl = isUrlSupportedByProvider(newUrl, attachmentDetails.providerName); - if (!providerStillValidForNewUrl) { - console.log(BG_LOG_PREFIX, `Tab ${tabId} loading new URL ${newUrl}. Provider ${attachmentDetails.providerName} may no longer be valid. Detaching.`); - await detachDebugger(tabId); + + const attachmentDetails = debuggerAttachedTabs.get(tabId); + if (attachmentDetails && attachmentDetails.isAttached && changeInfo.url && tab && tab.url) { + console.log(BG_LOG_PREFIX, `Tab ${tabId} updated. Old URL: ${changeInfo.url}, New URL: ${tab.url}. Checking debugger status.`); + + const providerStillValidForNewUrl = isUrlSupportedByProvider(tab.url, attachmentDetails.providerName); + + if (providerStillValidForNewUrl) { + console.log(BG_LOG_PREFIX, `Tab ${tabId} URL changed to ${tab.url}. Provider ${attachmentDetails.providerName} still valid. Re-initiating debugger attachment.`); + const oldProviderName = attachmentDetails.providerName; + const oldPatterns = attachmentDetails.patterns; + + await detachDebugger(tabId); + + try { + const updatedTabInfo = await chrome.tabs.get(tabId); + if (updatedTabInfo) { + console.log(BG_LOG_PREFIX, `Proactively re-attaching debugger to ${tabId} (${updatedTabInfo.url}) with provider ${oldProviderName}.`); + await attachDebuggerAndEnableFetch(tabId, oldProviderName, oldPatterns); + + if (processingRequest && lastRequestId !== null && tabId === currentRequestTargetTabId) { + const interruptedRequestDetails = pendingRequestDetails.get(lastRequestId); + if (interruptedRequestDetails) { + console.warn(BG_LOG_PREFIX, `Tab ${tabId} update (URL: ${tab.url}) may have interrupted processing for requestId: ${lastRequestId}. Attempting to resend after a delay.`); + + setTimeout(() => { + if (processingRequest && lastRequestId !== null && tabId === currentRequestTargetTabId && pendingRequestDetails.has(lastRequestId)) { + console.log(BG_LOG_PREFIX, `Re-forwarding command for interrupted requestId: ${lastRequestId} to tab ${tabId}`); + forwardCommandToContentScript({ + action: "SEND_CHAT_MESSAGE", + requestId: lastRequestId, + messageContent: interruptedRequestDetails.messageContent, + settings: interruptedRequestDetails.settings, + lastProcessedText: lastSuccessfullyProcessedMessageText + }); + } else { + console.log(BG_LOG_PREFIX, `Resend for ${lastRequestId} aborted; state changed before resend timeout. Current processing: ${processingRequest}, current lastReqId: ${lastRequestId}, current targetTab: ${currentRequestTargetTabId}, details still pending: ${pendingRequestDetails.has(lastRequestId)}`); + } + }, 2000); + } else { + console.warn(BG_LOG_PREFIX, `Tab ${tabId} update occurred while processing requestId: ${lastRequestId}, but no details found in pendingRequestDetails to resend. The request might have been cleared by another process.`); + + if (processingRequest && lastRequestId !== null && tabId === currentRequestTargetTabId) { + console.error(BG_LOG_PREFIX, `Critical state: Tab update for ${tabId} (target of ${lastRequestId}), but details missing. Forcing reset of processing state for ${lastRequestId}.`); + if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE_ERROR", + requestId: lastRequestId, + error: `Request ${lastRequestId} processing was interrupted by tab update and its details were lost. Cannot resend.` + })); + } + processingRequest = false; + currentRequestTargetTabId = null; + pendingRequestDetails.delete(lastRequestId); + const tabInfoForReset = debuggerAttachedTabs.get(tabId); + if (tabInfoForReset && tabInfoForReset.lastKnownRequestId === lastRequestId) { + tabInfoForReset.lastKnownRequestId = null; + } + processNextRequest(); + } + } + } + + if (processingRequest && lastRequestId !== null && tabId === currentRequestTargetTabId) { + const interruptedRequestDetails = pendingRequestDetails.get(lastRequestId); + if (interruptedRequestDetails) { + console.warn(BG_LOG_PREFIX, `Tab ${tabId} update (URL: ${tab.url}) may have interrupted processing for requestId: ${lastRequestId}. Attempting to resend after a delay.`); + + setTimeout(() => { + if (processingRequest && lastRequestId !== null && tabId === currentRequestTargetTabId && pendingRequestDetails.has(lastRequestId)) { + console.log(BG_LOG_PREFIX, `Re-forwarding command for interrupted requestId: ${lastRequestId} to tab ${tabId}`); + forwardCommandToContentScript({ + action: "SEND_CHAT_MESSAGE", + requestId: lastRequestId, + messageContent: interruptedRequestDetails.messageContent, + settings: interruptedRequestDetails.settings, + lastProcessedText: lastSuccessfullyProcessedMessageText + }); + } else { + console.log(BG_LOG_PREFIX, `Resend for ${lastRequestId} aborted; state changed before resend timeout. Current processing: ${processingRequest}, current lastReqId: ${lastRequestId}, current targetTab: ${currentRequestTargetTabId}, details still pending: ${pendingRequestDetails.has(lastRequestId)}`); + } + }, 2000); + } else { + console.warn(BG_LOG_PREFIX, `Tab ${tabId} update occurred while processing requestId: ${lastRequestId}, but no details found in pendingRequestDetails to resend. The request might have been cleared by another process.`); + if (processingRequest && lastRequestId !== null && tabId === currentRequestTargetTabId) { + console.error(BG_LOG_PREFIX, `Critical state: Tab update for ${tabId} (target of ${lastRequestId}), but details missing. Forcing reset of processing state for ${lastRequestId}.`); + if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE_ERROR", + requestId: lastRequestId, + error: `Request ${lastRequestId} processing was interrupted by tab update and its details were lost. Cannot resend.` + })); + } + processingRequest = false; + currentRequestTargetTabId = null; + pendingRequestDetails.delete(lastRequestId); + const tabInfoForReset = debuggerAttachedTabs.get(tabId); + if (tabInfoForReset && tabInfoForReset.lastKnownRequestId === lastRequestId) { + tabInfoForReset.lastKnownRequestId = null; + } + processNextRequest(); + } + } + } + } + } catch (error) { + console.warn(BG_LOG_PREFIX, `Error getting tab info for ${tabId} during re-attachment attempt:`, error.message); + } + + } else { + console.log(BG_LOG_PREFIX, `Tab ${tabId} URL changed to ${tab.url}. Provider ${attachmentDetails.providerName} no longer valid or URL not supported by provider. Detaching debugger.`); + await detachDebugger(tabId); + } + } else if (attachmentDetails && attachmentDetails.isAttached && changeInfo.status === 'loading' && tab && tab.url && !changeInfo.url) { + const newUrl = tab.url; + console.log(BG_LOG_PREFIX, `Tab ${tabId} is loading new URL: ${newUrl}. Checking debugger status.`); + const providerStillValidForNewUrl = isUrlSupportedByProvider(newUrl, attachmentDetails.providerName); + if (!providerStillValidForNewUrl) { + console.log(BG_LOG_PREFIX, `Tab ${tabId} loading new URL ${newUrl}. Provider ${attachmentDetails.providerName} may no longer be valid. Detaching.`); + await detachDebugger(tabId); + } } - // If provider is still valid, we'll let the 'complete' status handler above deal with re-attachment if needed, - // or rely on content script sending SET_DEBUGGER_TARGETS. - } }); -// Listen for messages from Content Scripts and Popup chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - console.log("BACKGROUND: Received message:", message.type || message.action, "from tabId:", sender.tab ? sender.tab.id : 'popup/unknown'); - - if (sender.tab && sender.tab.id) { - activeTabId = sender.tab.id; // User's original logic for activeTabId - console.log(`BACKGROUND: Updated activeTabId to ${activeTabId} from sender`); - } - - if (message.type === "SET_DEBUGGER_TARGETS") { - if (sender.tab && sender.tab.id) { - const tabId = sender.tab.id; - console.log(BG_LOG_PREFIX, `SET_DEBUGGER_TARGETS for tab ${tabId}, provider: ${message.providerName}, patterns:`, message.patterns); - attachDebuggerAndEnableFetch(tabId, message.providerName, message.patterns); - sendResponse({ status: "Debugger attachment initiated" }); - } else { - console.error(BG_LOG_PREFIX, "SET_DEBUGGER_TARGETS message received without valid sender.tab.id"); - sendResponse({ status: "Error: Missing tabId" }); - } - return true; - } - else if (message.type === "CHAT_RELAY_READY") { - console.log(`BACKGROUND: Content script ready in ${message.chatInterface} on tab ${sender.tab ? sender.tab.id : 'unknown'}`); - if (sender.tab && sender.tab.id) activeTabId = sender.tab.id; - sendResponse({ success: true }); - return true; // Indicate that sendResponse might be used (even if synchronously here) - } else if (message.action === "RESPONSE_CAPTURED") { - console.log(`BACKGROUND: Received captured response (OLD DOM METHOD) from content script on tab ${sender.tab ? sender.tab.id : 'unknown'} Request ID: ${message.requestId}`); - - if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { - console.log("BACKGROUND: Forwarding (OLD DOM) response to relay server:", message.response); - relaySocket.send(JSON.stringify({ - type: "CHAT_RESPONSE", - requestId: message.requestId, - response: message.response, - isFinal: true - })); - sendResponse({ success: true }); - - if (lastRequestId === message.requestId) { - processingRequest = false; - console.log("BACKGROUND: Reset processingRequest after (OLD DOM) RESPONSE_CAPTURED."); - processNextRequest(); - } + console.log("BACKGROUND: Received message:", message.type || message.action, "from tabId:", sender.tab ? sender.tab.id : 'popup/unknown'); - } else { - console.error("BACKGROUND: Relay WS not connected, cannot forward (OLD DOM) response"); - sendResponse({ success: false, error: "Relay WebSocket not connected" }); - if (lastRequestId === message.requestId) { - processingRequest = false; - } + if (sender.tab && sender.tab.id) { + activeTabId = sender.tab.id; + console.log(`BACKGROUND: Updated activeTabId to ${activeTabId} from sender`); } - return true; - } else if (message.action === "GET_CONNECTION_STATUS") { - const isConnected = relaySocket && relaySocket.readyState === WebSocket.OPEN; - sendResponse({ connected: isConnected }); - return true; // Indicate that sendResponse might be used - } else if (message.type === "CHAT_RESPONSE_FROM_DOM") { - console.log(`BACKGROUND: Received CHAT_RESPONSE_FROM_DOM from tab ${sender.tab ? sender.tab.id : 'unknown'} for requestId ${message.requestId}`); - const tabId = sender.tab ? sender.tab.id : null; - const tabInfo = tabId ? debuggerAttachedTabs.get(tabId) : null; - - if (tabInfo && tabInfo.lastKnownRequestId === message.requestId && processingRequest) { - if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { - relaySocket.send(JSON.stringify({ - type: "CHAT_RESPONSE_CHUNK", - requestId: message.requestId, - chunk: message.text, - isFinal: message.isFinal !== undefined ? message.isFinal : true - })); - relaySocket.send(JSON.stringify({ - type: "CHAT_RESPONSE_STREAM_ENDED", - requestId: message.requestId - })); - console.log(`BACKGROUND: Sent CHAT_RESPONSE_CHUNK (from DOM) and _STREAM_ENDED for app requestId: ${message.requestId}`); - sendResponse({ success: true, message: "DOM Response forwarded to relay." }); + + if (message.type === "SET_DEBUGGER_TARGETS") { + if (sender.tab && sender.tab.id) { + const tabId = sender.tab.id; + console.log(BG_LOG_PREFIX, `SET_DEBUGGER_TARGETS for tab ${tabId}, provider: ${message.providerName}, patterns:`, message.patterns); + attachDebuggerAndEnableFetch(tabId, message.providerName, message.patterns); + sendResponse({ status: "Debugger attachment initiated" }); } else { - console.error(`BACKGROUND: Relay WS not connected, cannot send DOM-captured response for requestId: ${message.requestId}`); - sendResponse({ success: false, error: "Relay WebSocket not connected." }); + console.error(BG_LOG_PREFIX, "SET_DEBUGGER_TARGETS message received without valid sender.tab.id"); + sendResponse({ status: "Error: Missing tabId" }); } - // Finalize this request processing - processingRequest = false; - if (tabInfo) tabInfo.lastKnownRequestId = null; // Clear for this specific tab op - console.log(`BACKGROUND: Reset processingRequest. Cleared lastKnownRequestId for tab ${tabId} after DOM response.`); - processNextRequest(); - } else { - console.warn(`BACKGROUND: Mismatched requestId or not processing for CHAT_RESPONSE_FROM_DOM. Current lastKnownRequestId: ${tabInfo ? tabInfo.lastKnownRequestId : 'N/A'}, processingRequest: ${processingRequest}, msg RequestId: ${message.requestId}`); - sendResponse({ success: false, error: "Mismatched requestId or not processing." }); + return true; } - return true; - } else if (message.type === "CHAT_RESPONSE_FROM_DOM_FAILED") { - console.error(`BACKGROUND: Received CHAT_RESPONSE_FROM_DOM_FAILED from tab ${sender.tab ? sender.tab.id : 'unknown'} for requestId ${message.requestId}: ${message.error}`); - const tabId = sender.tab ? sender.tab.id : null; - const tabInfo = tabId ? debuggerAttachedTabs.get(tabId) : null; + else if (message.type === "CHAT_RELAY_READY") { + console.log(`BACKGROUND: Content script ready in ${message.chatInterface} on tab ${sender.tab ? sender.tab.id : 'unknown'}`); + if (sender.tab && sender.tab.id) activeTabId = sender.tab.id; + sendResponse({ success: true }); + return true; + } else if (message.action === "RESPONSE_CAPTURED") { + console.log(`BACKGROUND: Received captured response (OLD DOM METHOD) from content script on tab ${sender.tab ? sender.tab.id : 'unknown'} Request ID: ${message.requestId}`); - if (tabInfo && tabInfo.lastKnownRequestId === message.requestId && processingRequest) { - if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { - relaySocket.send(JSON.stringify({ - type: "CHAT_RESPONSE_ERROR", - requestId: message.requestId, - error: `Failed to capture response from DOM on tab ${tabId}: ${message.error}` - })); - } - sendResponse({ success: true, message: "DOM failure noted and error sent to relay." }); - // Finalize this request processing - processingRequest = false; - if (tabInfo) tabInfo.lastKnownRequestId = null; - console.log(`BACKGROUND: Reset processingRequest. Cleared lastKnownRequestId for tab ${tabId} after DOM failure.`); - processNextRequest(); - } else { - console.warn(`BACKGROUND: Mismatched requestId or not processing for CHAT_RESPONSE_FROM_DOM_FAILED. Current lastKnownRequestId: ${tabInfo ? tabInfo.lastKnownRequestId : 'N/A'}, processingRequest: ${processingRequest}, msg RequestId: ${message.requestId}`); - sendResponse({ success: false, error: "Mismatched requestId or not processing for DOM failure." }); - } - return true; - } else if (message.type === "FINAL_RESPONSE_TO_RELAY") { - console.log(BG_LOG_PREFIX, `[REQ-${message.requestId}] RECEIVED FINAL_RESPONSE_TO_RELAY. FromTab: ${sender.tab ? sender.tab.id : 'N/A'}. HasError: ${!!message.error}. TextLength: ${message.text ? String(message.text).length : 'N/A'}. IsFinal: ${message.isFinal}. FullMsg:`, JSON.stringify(message).substring(0,500)); - const tabId = sender.tab ? sender.tab.id : null; - const tabInfo = tabId ? debuggerAttachedTabs.get(tabId) : null; - - // Update lastSuccessfullyProcessedMessageText regardless of current processing state, - // as this confirms a message text was fully processed by the AI. - const details = pendingRequestDetails.get(message.requestId); - if (details) { - if (typeof details.messageContent === 'string') { - lastSuccessfullyProcessedMessageText = details.messageContent; - console.log(`BACKGROUND: Updated lastSuccessfullyProcessedMessageText to: "${lastSuccessfullyProcessedMessageText.substring(0,50)}..." for completed requestId ${message.requestId}`); - } else { - console.log(`BACKGROUND: RequestId ${message.requestId} (messageContent type: ${typeof details.messageContent}) completed. lastSuccessfullyProcessedMessageText not updated with non-string content.`); - } - pendingRequestDetails.delete(message.requestId); - } else { - console.warn(`BACKGROUND: Received FINAL_RESPONSE_TO_RELAY for unknown requestId ${message.requestId} (not in pendingRequestDetails). Cannot update lastSuccessfullyProcessedMessageText accurately.`); - } - - // Check if this is the request we are currently processing for state reset - if (processingRequest && lastRequestId === message.requestId) { - if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { - if (message.error) { // Check if content.js sent an error (e.g., response too large) - console.error(BG_LOG_PREFIX, `Content script reported an error for requestId ${message.requestId}: ${message.error}`); - try { - relaySocket.send(JSON.stringify({ - type: "CHAT_RESPONSE_ERROR", - requestId: message.requestId, - error: message.error - })); - console.log(BG_LOG_PREFIX, `Sent CHAT_RESPONSE_ERROR to server for requestId ${message.requestId} due to content script error.`); - sendResponse({ success: true, message: "Error reported by content script sent to relay." }); - } catch (e) { - console.error(BG_LOG_PREFIX, `Error sending CHAT_RESPONSE_ERROR to relay for requestId ${message.requestId}:`, e); - sendResponse({ success: false, error: `Error sending CHAT_RESPONSE_ERROR to relay: ${e.message}` }); - } - } else { // No error from content.js, proceed to send data - try { - const responseText = message.text || ""; - console.log(BG_LOG_PREFIX, `Attempting to send FINAL CHAT_RESPONSE_CHUNK for requestId ${message.requestId}. Data length: ${responseText.length}`); - relaySocket.send(JSON.stringify({ - type: "CHAT_RESPONSE_CHUNK", - requestId: message.requestId, - chunk: responseText, - isFinal: true - })); - console.log(BG_LOG_PREFIX, `Attempting to send CHAT_RESPONSE_STREAM_ENDED for requestId ${message.requestId}`); - relaySocket.send(JSON.stringify({ - type: "CHAT_RESPONSE_STREAM_ENDED", - requestId: message.requestId - })); - console.log(BG_LOG_PREFIX, `Successfully sent FINAL CHAT_RESPONSE_CHUNK and _STREAM_ENDED for app requestId: ${message.requestId} to relaySocket.`); - sendResponse({ success: true, message: "Final response sent to relay." }); - } catch (e) { - console.error(BG_LOG_PREFIX, `Error during relaySocket.send() for FINAL response (requestId ${message.requestId}):`, e); - sendResponse({ success: false, error: `Error sending final response to relay: ${e.message}` }); - } - } - } else { - console.error(BG_LOG_PREFIX, `Relay WS not OPEN (state: ${relaySocket ? relaySocket.readyState : 'null'}), cannot send final response/error for app requestId: ${message.requestId}`); - sendResponse({ success: false, error: "Relay WebSocket not connected." }); - } - - // Finalize this request processing - console.log(BG_LOG_PREFIX, `Processing complete for command with app requestId: ${message.requestId} on tab ${tabId}`); - processingRequest = false; - if (tabInfo) tabInfo.lastKnownRequestId = null; - console.log(BG_LOG_PREFIX, `Reset processingRequest. Cleared lastKnownRequestId for tab ${tabId}.`); - processNextRequest(); - } else { - console.warn(`BACKGROUND: Received FINAL_RESPONSE_TO_RELAY for requestId ${message.requestId}, but not currently processing it (current: ${lastRequestId}, processing: ${processingRequest}). Ignoring.`); - sendResponse({ success: false, error: "Request ID mismatch or not processing." }); - } - return true; // Indicate async response potentially - } else if (message.type === "DUPLICATE_MESSAGE_HANDLED") { - console.log(`BACKGROUND: Content script handled requestId ${message.requestId} as a duplicate of text: "${message.originalText ? message.originalText.substring(0,50) : 'N/A'}..."`); - - // Update last successfully processed text because this text was confirmed as a duplicate of it. - lastSuccessfullyProcessedMessageText = message.originalText; - pendingRequestDetails.delete(message.requestId); // Clean up details map - console.log(`BACKGROUND: Updated lastSuccessfullyProcessedMessageText (due to duplicate) to: "${lastSuccessfullyProcessedMessageText ? lastSuccessfullyProcessedMessageText.substring(0,50) : 'N/A'}..."`); - - if (processingRequest && lastRequestId === message.requestId) { if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { + console.log("BACKGROUND: Forwarding (OLD DOM) response to relay server:", message.response); relaySocket.send(JSON.stringify({ type: "CHAT_RESPONSE", requestId: message.requestId, - response: `[ChatRelay Extension] Request to send duplicate message ("${message.originalText ? message.originalText.substring(0,100) : 'N/A'}") was detected and cleared from input. No message sent to AI.`, + response: message.response, isFinal: true })); - console.log(`BACKGROUND: Sent CHAT_RESPONSE (for duplicate) to server for requestId: ${message.requestId}.`); + sendResponse({ success: true }); + + if (lastRequestId === message.requestId) { + processingRequest = false; + console.log("BACKGROUND: Reset processingRequest after (OLD DOM) RESPONSE_CAPTURED."); + processNextRequest(); + } + } else { - console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE (for duplicate) for requestId: ${message.requestId}.`); + console.error("BACKGROUND: Relay WS not connected, cannot forward (OLD DOM) response"); + sendResponse({ success: false, error: "Relay WebSocket not connected" }); + if (lastRequestId === message.requestId) { + processingRequest = false; + } } - - processingRequest = false; - // lastRequestId remains, it's the ID of the last command *received* - // currentRequestText (if used) would be nulled here. - const tabInfo = sender.tab ? debuggerAttachedTabs.get(sender.tab.id) : null; - if (tabInfo && tabInfo.lastKnownRequestId === message.requestId) { - tabInfo.lastKnownRequestId = null; + return true; + } else if (message.action === "GET_CONNECTION_STATUS") { + const isConnected = relaySocket && relaySocket.readyState === WebSocket.OPEN; + sendResponse({ connected: isConnected }); + return true; + } else if (message.type === "CHAT_RESPONSE_FROM_DOM") { + console.log(`BACKGROUND: Received CHAT_RESPONSE_FROM_DOM from tab ${sender.tab ? sender.tab.id : 'unknown'} for requestId ${message.requestId}`); + const tabId = sender.tab ? sender.tab.id : null; + const tabInfo = tabId ? debuggerAttachedTabs.get(tabId) : null; + + if (tabInfo && tabInfo.lastKnownRequestId === message.requestId && processingRequest) { + if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE_CHUNK", + requestId: message.requestId, + chunk: message.text, + isFinal: message.isFinal !== undefined ? message.isFinal : true + })); + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE_STREAM_ENDED", + requestId: message.requestId + })); + console.log(`BACKGROUND: Sent CHAT_RESPONSE_CHUNK (from DOM) and _STREAM_ENDED for app requestId: ${message.requestId}`); + sendResponse({ success: true, message: "DOM Response forwarded to relay." }); + } else { + console.error(`BACKGROUND: Relay WS not connected, cannot send DOM-captured response for requestId: ${message.requestId}`); + sendResponse({ success: false, error: "Relay WebSocket not connected." }); + } + processingRequest = false; + if (tabInfo) tabInfo.lastKnownRequestId = null; + console.log(`BACKGROUND: Reset processingRequest. Cleared lastKnownRequestId for tab ${tabId} after DOM response.`); + processNextRequest(); + } else { + console.warn(`BACKGROUND: Mismatched requestId or not processing for CHAT_RESPONSE_FROM_DOM. Current lastKnownRequestId: ${tabInfo ? tabInfo.lastKnownRequestId : 'N/A'}, processingRequest: ${processingRequest}, msg RequestId: ${message.requestId}`); + sendResponse({ success: false, error: "Mismatched requestId or not processing." }); } + return true; + } else if (message.type === "CHAT_RESPONSE_FROM_DOM_FAILED") { + console.error(`BACKGROUND: Received CHAT_RESPONSE_FROM_DOM_FAILED from tab ${sender.tab ? sender.tab.id : 'unknown'} for requestId ${message.requestId}: ${message.error}`); + const tabId = sender.tab ? sender.tab.id : null; + const tabInfo = tabId ? debuggerAttachedTabs.get(tabId) : null; - console.log(`BACKGROUND: Reset processingRequest after DUPLICATE_MESSAGE_HANDLED for requestId: ${message.requestId}.`); - processNextRequest(); - } else { - console.warn(`BACKGROUND: Received DUPLICATE_MESSAGE_HANDLED for requestId ${message.requestId}, but not currently processing it or ID mismatch. Current lastRequestId: ${lastRequestId}, processing: ${processingRequest}. Still updated LSPMT.`); - // If it was an older request, its details are cleaned, LSPMT updated. Server informed if possible. - if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { - relaySocket.send(JSON.stringify({ - type: "CHAT_RESPONSE", - requestId: message.requestId, - response: `[ChatRelay Extension] An older/superseded request (ID: ${message.requestId}, Text: "${message.originalText ? message.originalText.substring(0,100) : 'N/A'}") was handled as a duplicate.`, - isFinal: true - })); + if (tabInfo && tabInfo.lastKnownRequestId === message.requestId && processingRequest) { + if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE_ERROR", + requestId: message.requestId, + error: `Failed to capture response from DOM on tab ${tabId}: ${message.error}` + })); + } + sendResponse({ success: true, message: "DOM failure noted and error sent to relay." }); + processingRequest = false; + if (tabInfo) tabInfo.lastKnownRequestId = null; + console.log(`BACKGROUND: Reset processingRequest. Cleared lastKnownRequestId for tab ${tabId} after DOM failure.`); + processNextRequest(); + } else { + console.warn(`BACKGROUND: Mismatched requestId or not processing for CHAT_RESPONSE_FROM_DOM_FAILED. Current lastKnownRequestId: ${tabInfo ? tabInfo.lastKnownRequestId : 'N/A'}, processingRequest: ${processingRequest}, msg RequestId: ${message.requestId}`); + sendResponse({ success: false, error: "Mismatched requestId or not processing for DOM failure." }); } - } - sendResponse({ success: true, message: "Duplicate handling acknowledged by background." }); - return true; - } else if (message.type === "USER_STOP_REQUEST") { - const requestIdToStop = message.requestId; - console.log(`BACKGROUND: Received USER_STOP_REQUEST for requestId: ${requestIdToStop}`); - let responseSent = false; // To ensure sendResponse is called once - - // Case 1: The request to stop is the currently processing one. - if (processingRequest && lastRequestId === requestIdToStop) { - console.log(`BACKGROUND: Initiating stop for currently processing request: ${lastRequestId}. Content script will send FINAL_RESPONSE_TO_RELAY.`); - if (activeTabId) { - chrome.tabs.sendMessage(activeTabId, { - action: "STOP_STREAMING", - requestId: lastRequestId - }, response => { - if (chrome.runtime.lastError) { - console.error(`BACKGROUND: Error sending STOP_STREAMING to tab ${activeTabId} for requestId ${lastRequestId}:`, chrome.runtime.lastError.message); + return true; + } else if (message.type === "FINAL_RESPONSE_TO_RELAY") { + console.log(BG_LOG_PREFIX, `[REQ-${message.requestId}] RECEIVED FINAL_RESPONSE_TO_RELAY. FromTab: ${sender.tab ? sender.tab.id : 'N/A'}. HasError: ${!!message.error}. TextLength: ${message.text ? String(message.text).length : 'N/A'}. IsFinal: ${message.isFinal}. FullMsg:`, JSON.stringify(message).substring(0, 500)); + const tabId = sender.tab ? sender.tab.id : null; + const tabInfo = tabId ? debuggerAttachedTabs.get(tabId) : null; + + const details = pendingRequestDetails.get(message.requestId); + if (details) { + if (typeof details.messageContent === 'string') { + lastSuccessfullyProcessedMessageText = details.messageContent; + console.log(`BACKGROUND: Updated lastSuccessfullyProcessedMessageText to: "${lastSuccessfullyProcessedMessageText.substring(0, 50)}..." for completed requestId ${message.requestId}`); + } else { + console.log(`BACKGROUND: RequestId ${message.requestId} (messageContent type: ${typeof details.messageContent}) completed. lastSuccessfullyProcessedMessageText not updated with non-string content.`); + } + pendingRequestDetails.delete(message.requestId); + } else { + console.warn(`BACKGROUND: Received FINAL_RESPONSE_TO_RELAY for unknown requestId ${message.requestId} (not in pendingRequestDetails). Cannot update lastSuccessfullyProcessedMessageText accurately.`); + } + + if (processingRequest && lastRequestId === message.requestId) { + if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { + if (message.error) { + console.error(BG_LOG_PREFIX, `Content script reported an error for requestId ${message.requestId}: ${message.error}`); + try { + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE_ERROR", + requestId: message.requestId, + error: message.error + })); + console.log(BG_LOG_PREFIX, `Sent CHAT_RESPONSE_ERROR to server for requestId ${message.requestId} due to content script error.`); + sendResponse({ success: true, message: "Error reported by content script sent to relay." }); + } catch (e) { + console.error(BG_LOG_PREFIX, `Error sending CHAT_RESPONSE_ERROR to relay for requestId ${message.requestId}:`, e); + sendResponse({ success: false, error: `Error sending CHAT_RESPONSE_ERROR to relay: ${e.message}` }); + } } else { - console.log(`BACKGROUND: Sent STOP_STREAMING to tab ${activeTabId} for requestId ${lastRequestId}. Content script ack:`, response); + try { + let responseText = message.text || ""; + + // Decode text if it was encoded by content script + if (message.encoded) { + responseText = decodeURIComponent(responseText); + } + + console.log(BG_LOG_PREFIX, `Attempting to send FINAL CHAT_RESPONSE_CHUNK for requestId ${message.requestId}. Data length: ${responseText.length}`); + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE_CHUNK", + requestId: message.requestId, + chunk: responseText, + isFinal: true + })); + console.log(BG_LOG_PREFIX, `Attempting to send CHAT_RESPONSE_STREAM_ENDED for requestId ${message.requestId}`); + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE_STREAM_ENDED", + requestId: message.requestId + })); + console.log(BG_LOG_PREFIX, `Successfully sent FINAL CHAT_RESPONSE_CHUNK and _STREAM_ENDED for app requestId: ${message.requestId} to relaySocket.`); + sendResponse({ success: true, message: "Final response sent to relay." }); + } catch (e) { + console.error(BG_LOG_PREFIX, `Error during relaySocket.send() for FINAL response (requestId ${message.requestId}):`, e); + sendResponse({ success: false, error: `Error sending final response to relay: ${e.message}` }); + } } - }); + } else { + console.error(BG_LOG_PREFIX, `Relay WS not OPEN (state: ${relaySocket ? relaySocket.readyState : 'null'}), cannot send final response/error for app requestId: ${message.requestId}`); + sendResponse({ success: false, error: "Relay WebSocket not connected." }); + } + + console.log(BG_LOG_PREFIX, `Processing complete for command with app requestId: ${message.requestId} on tab ${tabId}`); + processingRequest = false; + currentRequestTargetTabId = null; + if (tabInfo) tabInfo.lastKnownRequestId = null; + console.log(BG_LOG_PREFIX, `Reset processingRequest. Cleared lastKnownRequestId for tab ${tabId}.`); + processNextRequest(); } else { - console.warn(`BACKGROUND: Cannot send STOP_STREAMING for currently processing requestId ${lastRequestId}, activeTabId is null. This request might not be properly finalized by the provider.`); - // If no active tab, we can't tell content.js to stop. - // We should still inform the relay and clean up what we can, - // though the provider state might remain for this request. + console.warn(`BACKGROUND: Received FINAL_RESPONSE_TO_RELAY for requestId ${message.requestId}, but not currently processing it (current: ${lastRequestId}, processing: ${processingRequest}). Ignoring.`); + sendResponse({ success: false, error: "Request ID mismatch or not processing." }); + } + return true; + } else if (message.type === "DUPLICATE_MESSAGE_HANDLED") { + console.log(`BACKGROUND: Content script handled requestId ${message.requestId} as a duplicate of text: "${message.originalText ? message.originalText.substring(0, 50) : 'N/A'}..."`); + + lastSuccessfullyProcessedMessageText = message.originalText; + pendingRequestDetails.delete(message.requestId); + console.log(`BACKGROUND: Updated lastSuccessfullyProcessedMessageText (due to duplicate) to: "${lastSuccessfullyProcessedMessageText ? lastSuccessfullyProcessedMessageText.substring(0, 50) : 'N/A'}..."`); + + if (processingRequest && lastRequestId === message.requestId) { if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { relaySocket.send(JSON.stringify({ - type: "CHAT_RESPONSE_ERROR", - requestId: lastRequestId, - error: "Request cancelled by user (no active tab to signal provider)." + type: "CHAT_RESPONSE", + requestId: message.requestId, + response: `[ChatRelay Extension] Request to send duplicate message ("${message.originalText ? message.originalText.substring(0, 100) : 'N/A'}") was detected and cleared from input. No message sent to AI.`, + isFinal: true })); + console.log(`BACKGROUND: Sent CHAT_RESPONSE (for duplicate) to server for requestId: ${message.requestId}.`); + } else { + console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE (for duplicate) for requestId: ${message.requestId}.`); } - // Since we can't rely on FINAL_RESPONSE_TO_RELAY, we have to clean up here. + processingRequest = false; - pendingRequestDetails.delete(lastRequestId); - // lastSuccessfullyProcessedMessageText = null; // Consider if this should be reset - console.log(`BACKGROUND: Forcefully reset processingRequest for ${lastRequestId} due to USER_STOP_REQUEST with no active tab.`); - processNextRequest(); // Attempt to process next - } + currentRequestTargetTabId = null; + const tabInfo = sender.tab ? debuggerAttachedTabs.get(sender.tab.id) : null; + if (tabInfo && tabInfo.lastKnownRequestId === message.requestId) { + tabInfo.lastKnownRequestId = null; + } - // Inform relay server about cancellation (can be done early) - if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { - relaySocket.send(JSON.stringify({ - type: "CHAT_RESPONSE_ERROR", // Or a new type like "USER_CANCELLED_REQUEST" - requestId: lastRequestId, // Use lastRequestId as it's the one being processed - error: "Request cancelled by user." - })); - console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR (user cancelled) to server for currently processing requestId: ${lastRequestId}.`); + console.log(`BACKGROUND: Reset processingRequest after DUPLICATE_MESSAGE_HANDLED for requestId: ${message.requestId}.`); + processNextRequest(); + } else { + console.warn(`BACKGROUND: Received DUPLICATE_MESSAGE_HANDLED for requestId ${message.requestId}, but not currently processing it or ID mismatch. Current lastRequestId: ${lastRequestId}, processing: ${processingRequest}. Still updated LSPMT.`); + if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE", + requestId: message.requestId, + response: `[ChatRelay Extension] An older/superseded request (ID: ${message.requestId}, Text: "${message.originalText ? message.originalText.substring(0, 100) : 'N/A'}") was handled as a duplicate.`, + isFinal: true + })); + } } - - // IMPORTANT: Do NOT set processingRequest = false or clear lastRequestId details here. - // Let the FINAL_RESPONSE_TO_RELAY (triggered by provider.stopStreaming) handle the final state cleanup. - sendResponse({ success: true, message: `Stop initiated for currently processing request ${lastRequestId}. Waiting for finalization from content script.` }); - responseSent = true; - - // Case 2: The request to stop is in the pending queue (not actively processing). - } else { - const initialQueueLength = pendingRequests.length; - pendingRequests = pendingRequests.filter(req => req.requestId !== requestIdToStop); - if (pendingRequests.length < initialQueueLength) { - console.log(`BACKGROUND: Removed requestId ${requestIdToStop} from pendingRequests queue.`); - pendingRequestDetails.delete(requestIdToStop); // Clean up details for the queued item + sendResponse({ success: true, message: "Duplicate handling acknowledged by background." }); + return true; + } else if (message.type === "USER_STOP_REQUEST") { + const requestIdToStop = message.requestId; + console.log(`BACKGROUND: Received USER_STOP_REQUEST for requestId: ${requestIdToStop}`); + let responseSent = false; + + if (processingRequest && lastRequestId === requestIdToStop) { + console.log(`BACKGROUND: Initiating stop for currently processing request: ${lastRequestId}. Content script will send FINAL_RESPONSE_TO_RELAY.`); + if (activeTabId) { + chrome.tabs.sendMessage(activeTabId, { + action: "STOP_STREAMING", + requestId: lastRequestId + }, response => { + if (chrome.runtime.lastError) { + console.error(`BACKGROUND: Error sending STOP_STREAMING to tab ${activeTabId} for requestId ${lastRequestId}:`, chrome.runtime.lastError.message); + } else { + console.log(`BACKGROUND: Sent STOP_STREAMING to tab ${activeTabId} for requestId ${lastRequestId}. Content script ack:`, response); + } + }); + } else { + console.warn(`BACKGROUND: Cannot send STOP_STREAMING for currently processing requestId ${lastRequestId}, activeTabId is null. This request might not be properly finalized by the provider.`); + if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE_ERROR", + requestId: lastRequestId, + error: "Request cancelled by user (no active tab to signal provider)." + })); + } + processingRequest = false; + currentRequestTargetTabId = null; + pendingRequestDetails.delete(lastRequestId); + console.log(`BACKGROUND: Forcefully reset processingRequest for ${lastRequestId} due to USER_STOP_REQUEST with no active tab.`); + processNextRequest(); + } if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { - relaySocket.send(JSON.stringify({ + relaySocket.send(JSON.stringify({ type: "CHAT_RESPONSE_ERROR", - requestId: requestIdToStop, - error: `Request ${requestIdToStop} cancelled by user while in queue.` + requestId: lastRequestId, + error: "Request cancelled by user." })); - console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR (user cancelled in queue) to server for requestId: ${requestIdToStop}.`); + console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR (user cancelled) to server for currently processing requestId: ${lastRequestId}.`); } - if (!responseSent) sendResponse({ success: true, message: `Request ${requestIdToStop} removed from queue.` }); + + sendResponse({ success: true, message: `Stop initiated for currently processing request ${lastRequestId}. Waiting for finalization from content script.` }); responseSent = true; + + } else { + const initialQueueLength = pendingRequests.length; + pendingRequests = pendingRequests.filter(req => req.requestId !== requestIdToStop); + if (pendingRequests.length < initialQueueLength) { + console.log(`BACKGROUND: Removed requestId ${requestIdToStop} from pendingRequests queue.`); + pendingRequestDetails.delete(requestIdToStop); + + if (relaySocket && relaySocket.readyState === WebSocket.OPEN) { + relaySocket.send(JSON.stringify({ + type: "CHAT_RESPONSE_ERROR", + requestId: requestIdToStop, + error: `Request ${requestIdToStop} cancelled by user while in queue.` + })); + console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR (user cancelled in queue) to server for requestId: ${requestIdToStop}.`); + } + if (!responseSent) sendResponse({ success: true, message: `Request ${requestIdToStop} removed from queue.` }); + responseSent = true; + } } - } - if (!responseSent) { - console.warn(`BACKGROUND: USER_STOP_REQUEST for ${requestIdToStop}, but it was not actively processing nor found in the pending queue. Current active: ${lastRequestId}, processing: ${processingRequest}`); - sendResponse({ success: false, error: "Request not found processing or in queue." }); + if (!responseSent) { + console.warn(`BACKGROUND: USER_STOP_REQUEST for ${requestIdToStop}, but it was not actively processing nor found in the pending queue. Current active: ${lastRequestId}, processing: ${processingRequest}`); + sendResponse({ success: false, error: "Request not found processing or in queue." }); + } + return true; } - return true; - } - // IMPORTANT: Add other top-level else if (message.action === "SAVE_SETTINGS") etc. here if they exist outside this snippet }); -// Listen for storage changes to update the server URL chrome.storage.onChanged.addListener((changes, namespace) => { - if (namespace === 'sync') { - let needsReconnect = false; - if (changes.serverHost || changes.serverPort || changes.serverProtocol) { - needsReconnect = true; - } - - if (needsReconnect) { - console.log("BACKGROUND: Server settings changed, reconnecting..."); - if (relaySocket) { - relaySocket.close(); - } else { - loadSettingsAndConnect(); - } + if (namespace === 'sync') { + let needsReconnect = false; + if (changes.serverHost || changes.serverPort || changes.serverProtocol) { + needsReconnect = true; + } + + if (needsReconnect) { + console.log("BACKGROUND: Server settings changed, reconnecting..."); + if (relaySocket) { + relaySocket.close(); + } else { + loadSettingsAndConnect(); + } + } } - } }); -// Initial setup loadSettingsAndConnect(); -// Placeholder for providerUtils if it's not globally available from another script. -// In a real extension, this would likely be imported or part of a shared module. const providerUtils = { - _providers: {}, // providerName -> { instance, domains } - registerProvider: function(name, domains, instance) { + _providers: {}, + registerProvider: function (name, domains, instance) { this._providers[name] = { instance, domains }; - // console.log(BG_LOG_PREFIX, `Provider registered in background (simulated): ${name}`); }, - getProviderForUrl: function(url) { + getProviderForUrl: function (url) { for (const name in this._providers) { if (this._providers[name].domains.some(domain => url.includes(domain))) { return this._providers[name].instance; @@ -787,10 +793,7 @@ const providerUtils = { } return null; }, - // Simulate AIStudioProvider registration for isUrlSupportedByProvider - // This would normally happen if provider-utils.js was also loaded in background context - // or if this info was passed/stored differently. - _initializeSimulatedProviders: function() { + _initializeSimulatedProviders: function () { this.registerProvider("AIStudioProvider", ["aistudio.google.com"], { name: "AIStudioProvider" }); this.registerProvider("GeminiProvider", ["gemini.google.com"], { name: "GeminiProvider" }); this.registerProvider("GeminiProvider", ["chatgpt.com"], { name: "ChatGPTProvider" }); @@ -798,12 +801,11 @@ const providerUtils = { } }; -providerUtils._initializeSimulatedProviders(); // Call to populate for the helper +providerUtils._initializeSimulatedProviders(); console.log("BACKGROUND: AI Chat Relay: Background Service Worker started."); -// ===== DEBUGGER LOGIC ===== async function attachDebuggerAndEnableFetch(tabId, providerName, patterns) { if (!tabId || !patterns || patterns.length === 0) { console.error(BG_LOG_PREFIX, `attachDebuggerAndEnableFetch: Invalid parameters for tab ${tabId}. Patterns:`, patterns); @@ -831,17 +833,17 @@ async function attachDebuggerAndEnableFetch(tabId, providerName, patterns) { patterns: patterns, isFetchEnabled: false, isAttached: true, - lastKnownRequestId: null + lastKnownRequestId: null }); resolve(); }); }); } - + const currentTabDataForPatterns = debuggerAttachedTabs.get(tabId); if (currentTabDataForPatterns) { - currentTabDataForPatterns.patterns = patterns; - currentTabDataForPatterns.providerName = providerName; + currentTabDataForPatterns.patterns = patterns; + currentTabDataForPatterns.providerName = providerName; } console.log(BG_LOG_PREFIX, `Enabling Fetch domain for tab ${tabId} with patterns:`, patterns); @@ -882,15 +884,14 @@ async function detachDebugger(tabId) { console.log(BG_LOG_PREFIX, `Successfully detached debugger from tab ${tabId}`); } debuggerAttachedTabs.delete(tabId); - resolve(); // Resolve even if detach had an error, as we've cleaned up map + resolve(); }); }); - } catch (error) { // Catch errors from the Promise constructor itself or unhandled rejections + } catch (error) { console.error(BG_LOG_PREFIX, `Exception during detach for tab ${tabId}:`, error); - debuggerAttachedTabs.delete(tabId); // Ensure cleanup + debuggerAttachedTabs.delete(tabId); } } else { - // If not attached or no details, still ensure it's not in the map debuggerAttachedTabs.delete(tabId); } } @@ -919,8 +920,6 @@ chrome.debugger.onEvent.addListener((debuggeeId, message, params) => { const tabId = debuggeeId.tabId; const tabInfo = debuggerAttachedTabs.get(tabId); - // DEVELOPER ACTION: This parsing function needs to be robustly implemented - // based on consistent observation of the AI Studio response structure. function parseAiStudioResponse(jsonString) { try { const parsed = JSON.parse(jsonString); @@ -933,18 +932,16 @@ chrome.debugger.onEvent.addListener((debuggeeId, message, params) => { for (const innerMostArray of candidateBlock[0][0][0][0]) { if (Array.isArray(innerMostArray) && innerMostArray.length > 1 && typeof innerMostArray[1] === 'string') { const textSegment = innerMostArray[1]; - // Basic heuristic to filter out "thought process" or similar meta-commentary. - // This will need refinement based on actual response variations. if (!textSegment.toLowerCase().includes("thinking process") && !textSegment.toLowerCase().includes("thought process") && - !textSegment.startsWith("1.") && // Avoid numbered list from thoughts + !textSegment.startsWith("1.") && !textSegment.startsWith("2.") && !textSegment.startsWith("3.") && !textSegment.startsWith("4.") && !textSegment.startsWith("5.") && !textSegment.startsWith("6.") && textSegment.trim() !== "**") { - combinedText += textSegment; // Concatenate, newlines are part of the text + combinedText += textSegment; } } } @@ -953,14 +950,13 @@ chrome.debugger.onEvent.addListener((debuggeeId, message, params) => { } } } - // Cleanup common markdown/formatting that might not be desired for relay let cleanedMessage = combinedText.replace(/\*\*/g, "").replace(/\\n/g, "\n").replace(/\n\s*\n/g, '\n').trim(); - + if (cleanedMessage) { console.log(BG_LOG_PREFIX, "Parsed AI Studio response to (first 100 chars):", cleanedMessage.substring(0, 100)); return cleanedMessage; } else { - console.warn(BG_LOG_PREFIX, "Parsing AI Studio response yielded empty text. Original (first 200 chars):", jsonString.substring(0,200)); + console.warn(BG_LOG_PREFIX, "Parsing AI Studio response yielded empty text. Original (first 200 chars):", jsonString.substring(0, 200)); } } catch (e) { console.error(BG_LOG_PREFIX, "Error parsing AI Studio response JSON:", e, "Original string (first 200 chars):", jsonString.substring(0, 200)); @@ -996,60 +992,51 @@ chrome.debugger.onEvent.addListener((debuggeeId, message, params) => { } console.log(BG_LOG_PREFIX, `Raw responseBodyData for debugger requestId ${params.requestId} (first 200 chars):`, JSON.stringify(responseBodyData).substring(0, 200) + "..."); - const rawBodyText = responseBodyData.base64Encoded ? atob(responseBodyData.body) : responseBodyData.body; - + const rawBodyText = responseBodyData.base64Encoded ? new TextDecoder('utf-8').decode(Uint8Array.from(atob(responseBodyData.body), c => c.charCodeAt(0))) : responseBodyData.body; + if (rawBodyText === undefined || rawBodyText === null) { console.error(BG_LOG_PREFIX, `Extracted rawBodyText is undefined or null for debugger requestId ${params.requestId}.`); return; } - - // Prioritize tabInfo.lastKnownRequestId if available and matches the global lastRequestId - // Otherwise, use the global lastRequestId if we are in a processing state. + let tempRequestId = tabInfo ? tabInfo.lastKnownRequestId : null; - if (processingRequest && lastRequestId !== null) { // lastRequestId is the app's current global request ID + if (processingRequest && lastRequestId !== null) { if (tempRequestId !== null && tempRequestId === lastRequestId) { currentOperationRequestId = tempRequestId; console.log(BG_LOG_PREFIX, `Using tabInfo.lastKnownRequestId: ${currentOperationRequestId} for debugger event on tab ${tabId} (debugger requestId ${params.requestId})`); } else { - currentOperationRequestId = lastRequestId; // Fallback to global if tabInfo's doesn't match or is null + currentOperationRequestId = lastRequestId; console.warn(BG_LOG_PREFIX, `Using global lastRequestId: ${currentOperationRequestId} for debugger event on tab ${tabId} (debugger requestId ${params.requestId}). TabInfo had: ${tempRequestId}.`); } } else if (tempRequestId !== null) { - // Not in a global processingRequest state, but tabInfo has an ID. This might be a stray event. currentOperationRequestId = tempRequestId; - console.warn(BG_LOG_PREFIX, `Not in global processingRequest, but using tabInfo.lastKnownRequestId: ${currentOperationRequestId} for debugger event on tab ${tabId} (debugger requestId ${params.requestId}). This might be unexpected.`); + console.warn(BG_LOG_PREFIX, `Not in global processingRequest, but using tabInfo.lastKnownRequestId: ${currentOperationRequestId} for debugger event on tab ${tabId} (debugger requestId ${params.requestId}). This might be unexpected.`); } else { currentOperationRequestId = null; } - // Check specifically for null/undefined, as 0 is a valid ID but falsy in JS if (currentOperationRequestId === null || currentOperationRequestId === undefined) { console.warn(BG_LOG_PREFIX, `Could not determine currentOperationRequestId for debugger event on tab ${tabId} (debugger requestId ${params.requestId}). Global lastRequestId: ${lastRequestId}, processingRequest: ${processingRequest}, tabInfo.lastKnownRequestId: ${tabInfo ? tabInfo.lastKnownRequestId : 'N/A'}. Ignoring body.`); return; } - + if (rawBodyText === "") { console.warn(BG_LOG_PREFIX, `Received empty rawBodyText for app requestId ${currentOperationRequestId} (debugger requestId ${params.requestId}). Not processing further for this event, waiting for potential subsequent data.`); return; } console.log(BG_LOG_PREFIX, `Raw bodyText for tab ${tabId}, debugger requestId ${params.requestId} (first 100 chars):`, rawBodyText.substring(0, 100)); - - // User wants rawBodyText (decoded JSON string) to be sent directly. - // The parseAiStudioResponse function will be bypassed for this debugger flow. + const dataToSend = rawBodyText; - console.log(BG_LOG_PREFIX, `Data to send for app requestId ${currentOperationRequestId} (first 100 chars): '${dataToSend ? dataToSend.substring(0,100) : "[EMPTY_DATA]"}'`); + console.log(BG_LOG_PREFIX, `Data to send for app requestId ${currentOperationRequestId} (first 100 chars): '${dataToSend ? dataToSend.substring(0, 100) : "[EMPTY_DATA]"}'`); - // CRITICAL CHECK: Only proceed if this requestId is still considered active/pending by background.js if (currentOperationRequestId !== null && currentOperationRequestId !== undefined && pendingRequestDetails.has(currentOperationRequestId)) { - if (tabId) { // tabId should be valid if we reached here from debuggeeId + if (tabId) { const messageToSend = { type: "DEBUGGER_RESPONSE", requestId: currentOperationRequestId, - data: dataToSend, // Send the raw decoded JSON string - // isFinal: true // The provider's parseDebuggerResponse will determine true finality from content. - // Background sends true to indicate this debugger event (HTTP response) is complete. + data: dataToSend, isFinal: true }; console.log(BG_LOG_PREFIX, `Attempting to send DEBUGGER_RESPONSE to tab ${tabId} for app requestId ${currentOperationRequestId}. Message object:`, JSON.stringify(messageToSend)); @@ -1065,10 +1052,9 @@ chrome.debugger.onEvent.addListener((debuggeeId, message, params) => { error: `Failed to send/ack DEBUGGER_RESPONSE to content script for requestId ${currentOperationRequestId}: ${errorMessage}` })); } - // If sending to content script fails, and it's the active request, we must clean up. if (processingRequest && lastRequestId === currentOperationRequestId) { processingRequest = false; - pendingRequestDetails.delete(currentOperationRequestId); // Ensure it's removed + pendingRequestDetails.delete(currentOperationRequestId); const tabInfoForReset = debuggerAttachedTabs.get(tabId); if (tabInfoForReset && tabInfoForReset.lastKnownRequestId === currentOperationRequestId) { tabInfoForReset.lastKnownRequestId = null; @@ -1086,7 +1072,6 @@ chrome.debugger.onEvent.addListener((debuggeeId, message, params) => { console.warn(BG_LOG_PREFIX, `Skipping sending DEBUGGER_RESPONSE for app requestId ${currentOperationRequestId} (debugger requestId ${params.requestId}) because it's no longer in pendingRequestDetails or ID is null/undefined. Tab: ${tabId}.`); } } finally { - // We still need to continue the intercepted request in the browser, regardless of whether we processed its body. console.log(BG_LOG_PREFIX, `[FINALLY] Continuing debugger request ${params.requestId}.`); chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId }, () => { if (chrome.runtime.lastError) { diff --git a/extension/content.js b/extension/content.js index 0a30933..f0fdb9e 100644 --- a/extension/content.js +++ b/extension/content.js @@ -1,41 +1,19 @@ -/* - * Chat Relay: Relay for AI Chat Interfaces - * Copyright (C) 2025 Jamison Moore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - */ -// AI Chat Relay - Content Script - -// Prefix for console logs + const CS_LOG_PREFIX = '[CS CONTENT]'; console.log(CS_LOG_PREFIX, "Content Script Injected & Loaded"); -// Global state -let provider = null; // This will be set by initializeContentRelay +let provider = null; let setupComplete = false; let currentRequestId = null; -let processingMessage = false; // Flag to track if we're currently processing a message -let responseMonitoringTimers = []; // Keep track of all monitoring timers -let captureAttempts = 0; // Track how many capture attempts we've made -const MAX_CAPTURE_ATTEMPTS = 30; // Maximum number of capture attempts -const CAPTURE_DELAY = 1000; // 1 second between capture attempts +let processingMessage = false; +let responseMonitoringTimers = []; +let captureAttempts = 0; +const MAX_CAPTURE_ATTEMPTS = 30; +const CAPTURE_DELAY = 1000; -// Helper function to find potential input fields and buttons function findPotentialSelectors() { console.log(CS_LOG_PREFIX, "Searching for potential input fields and buttons..."); - - // Find all textareas + const textareas = document.querySelectorAll('textarea'); console.log(CS_LOG_PREFIX, "Found textareas:", textareas.length); textareas.forEach((textarea, index) => { @@ -47,8 +25,7 @@ function findPotentialSelectors() { name: textarea.name }); }); - - // Find all input fields + const inputs = document.querySelectorAll('input[type="text"]'); console.log(CS_LOG_PREFIX, "Found text inputs:", inputs.length); inputs.forEach((input, index) => { @@ -60,8 +37,7 @@ function findPotentialSelectors() { name: input.name }); }); - - // Find all buttons + const buttons = document.querySelectorAll('button'); console.log(CS_LOG_PREFIX, "Found buttons:", buttons.length); buttons.forEach((button, index) => { @@ -75,109 +51,102 @@ function findPotentialSelectors() { } function initializeContentRelay() { - if (setupComplete) { - console.log(CS_LOG_PREFIX, "Initialization already attempted or complete."); - return; - } - console.log(CS_LOG_PREFIX, 'Initializing content relay...'); + if (setupComplete) { + console.log(CS_LOG_PREFIX, "Initialization already attempted or complete."); + return; + } + console.log(CS_LOG_PREFIX, 'Initializing content relay...'); - // Provider Detection - if (window.providerUtils) { - const detectedProvider = window.providerUtils.detectProvider(window.location.hostname); // New detection method - provider = detectedProvider; // Update the global provider instance + if (window.providerUtils) { + const detectedProvider = window.providerUtils.detectProvider(window.location.hostname); + provider = detectedProvider; - console.log(CS_LOG_PREFIX, 'Detected provider:', provider ? provider.name : 'None'); + console.log(CS_LOG_PREFIX, 'Detected provider:', provider ? provider.name : 'None'); - if (provider && typeof provider.getStreamingApiPatterns === 'function') { - const patternsFromProvider = provider.getStreamingApiPatterns(); - console.log(CS_LOG_PREFIX, 'Retrieved patterns from provider:', patternsFromProvider); + if (provider && typeof provider.getStreamingApiPatterns === 'function') { + const patternsFromProvider = provider.getStreamingApiPatterns(); + console.log(CS_LOG_PREFIX, 'Retrieved patterns from provider:', patternsFromProvider); - if (patternsFromProvider && patternsFromProvider.length > 0) { - chrome.runtime.sendMessage({ - type: "SET_DEBUGGER_TARGETS", - providerName: provider.name, - patterns: patternsFromProvider - }, response => { - if (chrome.runtime.lastError) { - console.error(CS_LOG_PREFIX, 'Error sending SET_DEBUGGER_TARGETS:', chrome.runtime.lastError.message); - } else { - console.log(CS_LOG_PREFIX, 'SET_DEBUGGER_TARGETS message sent, response:', response); - } - }); - } else { - console.log(CS_LOG_PREFIX, 'No patterns returned by provider or patterns array is empty.'); - } - } else { - if (provider) { - console.log(CS_LOG_PREFIX, `Provider '${provider.name}' found, but getStreamingApiPatterns method is missing or not a function.`); - } else { - console.log(CS_LOG_PREFIX, 'No current provider instance found to get patterns from.'); - } - } + if (patternsFromProvider && patternsFromProvider.length > 0) { + chrome.runtime.sendMessage({ + type: "SET_DEBUGGER_TARGETS", + providerName: provider.name, + patterns: patternsFromProvider + }, response => { + if (chrome.runtime.lastError) { + console.error(CS_LOG_PREFIX, 'Error sending SET_DEBUGGER_TARGETS:', chrome.runtime.lastError.message); + } else { + console.log(CS_LOG_PREFIX, 'SET_DEBUGGER_TARGETS message sent, response:', response); + } + }); + } else { + console.log(CS_LOG_PREFIX, 'No patterns returned by provider or patterns array is empty.'); + } } else { - console.error(CS_LOG_PREFIX, 'providerUtils not found. Cannot detect provider or send patterns.'); + if (provider) { + console.log(CS_LOG_PREFIX, `Provider '${provider.name}' found, but getStreamingApiPatterns method is missing or not a function.`); + } else { + console.log(CS_LOG_PREFIX, 'No current provider instance found to get patterns from.'); + } } + } else { + console.error(CS_LOG_PREFIX, 'providerUtils not found. Cannot detect provider or send patterns.'); + } - // Send CHAT_RELAY_READY (always, after attempting provider setup) - chrome.runtime.sendMessage({ - type: "CHAT_RELAY_READY", - chatInterface: provider ? provider.name : "unknown" // Add provider name - }, response => { - if (chrome.runtime.lastError) { - console.error(CS_LOG_PREFIX, 'Error sending CHAT_RELAY_READY:', chrome.runtime.lastError.message); - } else { - console.log(CS_LOG_PREFIX, 'CHAT_RELAY_READY message sent, response:', response); - } - }); - - // Setup message listeners (will be called later, once, via setupMessageListeners) - - // If a provider is detected, proceed with provider-specific setup after a delay - if (provider) { - console.log(CS_LOG_PREFIX, `Proceeding with provider-specific setup for: ${provider.name}`); - setTimeout(() => { - // Double check setupComplete flag in case of async issues or rapid calls, though less likely here. - if (!setupComplete) { - findPotentialSelectors(); - setupAutomaticResponseCapture(); - startElementPolling(); - console.log(CS_LOG_PREFIX, "Provider-specific DOM setup (response capture, polling) initiated after delay."); - } - }, 2000); // Delay to allow page elements to fully render + chrome.runtime.sendMessage({ + type: "CHAT_RELAY_READY", + chatInterface: provider ? provider.name : "unknown" + }, response => { + if (chrome.runtime.lastError) { + console.error(CS_LOG_PREFIX, 'Error sending CHAT_RELAY_READY:', chrome.runtime.lastError.message); } else { - console.warn(CS_LOG_PREFIX, "No provider detected. Some provider-specific features (response capture, element polling) will not be initialized."); + console.log(CS_LOG_PREFIX, 'CHAT_RELAY_READY message sent, response:', response); } - - setupComplete = true; - console.log(CS_LOG_PREFIX, "Content relay initialization sequence finished."); + }); + + + if (provider) { + console.log(CS_LOG_PREFIX, `Proceeding with provider-specific setup for: ${provider.name}`); + setTimeout(() => { + if (!setupComplete) { + findPotentialSelectors(); + setupAutomaticResponseCapture(); + startElementPolling(); + console.log(CS_LOG_PREFIX, "Provider-specific DOM setup (response capture, polling) initiated after delay."); + } + }, 2000); + } else { + console.warn(CS_LOG_PREFIX, "No provider detected. Some provider-specific features (response capture, element polling) will not be initialized."); + } + + setupComplete = true; + console.log(CS_LOG_PREFIX, "Content relay initialization sequence finished."); } -// Poll for elements that might be loaded dynamically function startElementPolling() { if (!provider) { console.warn(CS_LOG_PREFIX, "Cannot start element polling: no provider detected."); return; } console.log(CS_LOG_PREFIX, "Starting element polling..."); - - // Check every 2 seconds for the input field and send button + const pollingInterval = setInterval(() => { - if (!provider) { // Provider might have been lost or was never there - clearInterval(pollingInterval); - console.warn(CS_LOG_PREFIX, "Stopping element polling: provider became unavailable."); - return; + if (!provider) { + clearInterval(pollingInterval); + console.warn(CS_LOG_PREFIX, "Stopping element polling: provider became unavailable."); + return; } const inputField = document.querySelector(provider.inputSelector); const sendButton = document.querySelector(provider.sendButtonSelector); - + if (inputField) { console.log(CS_LOG_PREFIX, "Found input field:", inputField); } - + if (sendButton) { console.log(CS_LOG_PREFIX, "Found send button:", sendButton); } - + if (inputField && sendButton) { console.log(CS_LOG_PREFIX, "Found all required elements, stopping polling"); clearInterval(pollingInterval); @@ -185,18 +154,15 @@ function startElementPolling() { }, 2000); } -// Function to send a message to the chat interface function sendChatMessage(text) { if (!provider) { console.error(CS_LOG_PREFIX, "Cannot send chat message: No provider configured."); - processingMessage = false; // Reset flag + processingMessage = false; return false; } - // Try to send the message with retries - return sendChatMessageWithRetry(text, 5); // Try up to 5 times + return sendChatMessageWithRetry(text, 5); } -// Helper function to send a message with retries function sendChatMessageWithRetry(text, maxRetries, currentRetry = 0) { if (!provider) { console.error(CS_LOG_PREFIX, `Cannot send chat message with retry (attempt ${currentRetry + 1}/${maxRetries}): No provider.`); @@ -212,13 +178,13 @@ function sendChatMessageWithRetry(text, maxRetries, currentRetry = 0) { setTimeout(() => { sendChatMessageWithRetry(text, maxRetries, currentRetry + 1); }, 1000); - return true; + return true; } console.error(CS_LOG_PREFIX, "Could not find input field after all retries"); - processingMessage = false; + processingMessage = false; return false; } - + const sendButton = document.querySelector(provider.sendButtonSelector); if (!sendButton) { console.log(CS_LOG_PREFIX, `Could not find send button (attempt ${currentRetry + 1}/${maxRetries})`); @@ -227,31 +193,31 @@ function sendChatMessageWithRetry(text, maxRetries, currentRetry = 0) { setTimeout(() => { sendChatMessageWithRetry(text, maxRetries, currentRetry + 1); }, 1000); - return true; + return true; } console.error(CS_LOG_PREFIX, "Could not find send button after all retries"); - processingMessage = false; + processingMessage = false; return false; } - + const result = provider.sendChatMessage(text, inputField, sendButton); - + if (result) { - console.log(CS_LOG_PREFIX, "Message sent successfully via provider."); - if (provider.shouldSkipResponseMonitoring && provider.shouldSkipResponseMonitoring()) { - console.log(CS_LOG_PREFIX, `Provider ${provider.name} has requested to skip response monitoring.`); - processingMessage = false; // Message sent, no monitoring, so reset. - } else { - console.log(CS_LOG_PREFIX, `Waiting ${CAPTURE_DELAY/1000} seconds before starting to monitor for responses...`); - const timer = setTimeout(() => { - console.log(CS_LOG_PREFIX, "Starting to monitor for responses now"); - startMonitoringForResponse(); - }, CAPTURE_DELAY); - responseMonitoringTimers.push(timer); - } + console.log(CS_LOG_PREFIX, "Message sent successfully via provider."); + if (provider.shouldSkipResponseMonitoring && provider.shouldSkipResponseMonitoring()) { + console.log(CS_LOG_PREFIX, `Provider ${provider.name} has requested to skip response monitoring.`); + processingMessage = false; + } else { + console.log(CS_LOG_PREFIX, `Waiting ${CAPTURE_DELAY / 1000} seconds before starting to monitor for responses...`); + const timer = setTimeout(() => { + console.log(CS_LOG_PREFIX, "Starting to monitor for responses now"); + startMonitoringForResponse(); + }, CAPTURE_DELAY); + responseMonitoringTimers.push(timer); + } } else { - console.error(CS_LOG_PREFIX, "Provider reported failure sending message."); - processingMessage = false; // Reset on failure + console.error(CS_LOG_PREFIX, "Provider reported failure sending message."); + processingMessage = false; } return result; } catch (error) { @@ -261,49 +227,47 @@ function sendChatMessageWithRetry(text, maxRetries, currentRetry = 0) { setTimeout(() => { sendChatMessageWithRetry(text, maxRetries, currentRetry + 1); }, 1000); - return true; + return true; } - processingMessage = false; + processingMessage = false; return false; } } -// Function to start monitoring for a response function startMonitoringForResponse() { if (!provider || !provider.responseSelector || !provider.getResponseText) { console.error(CS_LOG_PREFIX, "Cannot monitor for response: Provider or necessary provider methods/selectors are not configured."); - processingMessage = false; // Can't monitor, so reset. + processingMessage = false; return; } console.log(CS_LOG_PREFIX, "Starting response monitoring process..."); - captureAttempts = 0; // Reset capture attempts for this new monitoring session + captureAttempts = 0; const attemptCapture = () => { if (!processingMessage && currentRequestId === null) { console.log(CS_LOG_PREFIX, "Response monitoring stopped because processingMessage is false and currentRequestId is null (likely request completed or cancelled)."); - return; // Stop if no longer processing a message + return; } - + if (captureAttempts >= MAX_CAPTURE_ATTEMPTS) { console.error(CS_LOG_PREFIX, "Maximum response capture attempts reached. Stopping monitoring."); - // Send a timeout/error message back to the background script - if (currentRequestId !== null) { // Ensure there's a request ID to report error for - chrome.runtime.sendMessage({ - type: "FINAL_RESPONSE_TO_RELAY", - requestId: currentRequestId, - error: "Response capture timed out in content script.", - isFinal: true // Treat as final to unblock server - }, response => { - if (chrome.runtime.lastError) { - console.error(CS_LOG_PREFIX, 'Error sending capture timeout error:', chrome.runtime.lastError.message); - } else { - console.log(CS_LOG_PREFIX, 'Capture timeout error sent to background, response:', response); - } - }); + if (currentRequestId !== null) { + chrome.runtime.sendMessage({ + type: "FINAL_RESPONSE_TO_RELAY", + requestId: currentRequestId, + error: "Response capture timed out in content script.", + isFinal: true + }, response => { + if (chrome.runtime.lastError) { + console.error(CS_LOG_PREFIX, 'Error sending capture timeout error:', chrome.runtime.lastError.message); + } else { + console.log(CS_LOG_PREFIX, 'Capture timeout error sent to background, response:', response); + } + }); } processingMessage = false; - currentRequestId = null; // Clear current request ID as it timed out + currentRequestId = null; return; } @@ -313,45 +277,40 @@ function startMonitoringForResponse() { const responseElement = document.querySelector(provider.responseSelector); if (responseElement) { const responseText = provider.getResponseText(responseElement); - const isFinal = provider.isResponseComplete ? provider.isResponseComplete(responseElement) : false; // Default to false if not implemented + const isFinal = provider.isResponseComplete ? provider.isResponseComplete(responseElement) : false; console.log(CS_LOG_PREFIX, `Captured response text (length: ${responseText.length}), isFinal: ${isFinal}`); - - // Send to background + chrome.runtime.sendMessage({ - type: "FINAL_RESPONSE_TO_RELAY", // Or a new type like "PARTIAL_RESPONSE" if needed + type: "FINAL_RESPONSE_TO_RELAY", requestId: currentRequestId, text: responseText, isFinal: isFinal }, response => { - if (chrome.runtime.lastError) { - console.error(CS_LOG_PREFIX, 'Error sending response data to background:', chrome.runtime.lastError.message); - } else { - console.log(CS_LOG_PREFIX, 'Response data sent to background, response:', response); - } + if (chrome.runtime.lastError) { + console.error(CS_LOG_PREFIX, 'Error sending response data to background:', chrome.runtime.lastError.message); + } else { + console.log(CS_LOG_PREFIX, 'Response data sent to background, response:', response); + } }); if (isFinal) { console.log(CS_LOG_PREFIX, "Final response detected. Stopping monitoring."); - processingMessage = false; // Reset flag as processing is complete - // currentRequestId will be cleared by handleProviderResponse or if a new message comes - return; + processingMessage = false; + return; } } else { console.log(CS_LOG_PREFIX, "Response element not found yet."); } - // Continue polling const timer = setTimeout(attemptCapture, CAPTURE_DELAY); responseMonitoringTimers.push(timer); }; - // Initial call to start the process attemptCapture(); } -// Function to set up automatic response capture using MutationObserver function setupAutomaticResponseCapture() { if (!provider || !provider.responseContainerSelector || typeof provider.handleMutation !== 'function') { console.warn(CS_LOG_PREFIX, "Cannot set up automatic response capture: Provider or necessary provider methods/selectors are not configured."); @@ -364,47 +323,35 @@ function setupAutomaticResponseCapture() { if (!targetNode) { console.warn(CS_LOG_PREFIX, `Response container element ('${provider.responseContainerSelector}') not found. MutationObserver not started. Will rely on polling or debugger.`); - // Optionally, retry finding the targetNode after a delay, or fall back to polling exclusively. - // For now, we just warn and don't start the observer. return; } const config = { childList: true, subtree: true, characterData: true }; const callback = (mutationsList, observer) => { - // If not processing a message, or no current request, don't do anything. - // This check is crucial to prevent processing mutations when not expected. if (!processingMessage || currentRequestId === null) { - // console.log(CS_LOG_PREFIX, "MutationObserver: Ignoring mutation, not actively processing a message or no currentRequestId."); - return; + return; } - - // Let the provider handle the mutation and decide if it's relevant - // The provider's handleMutation should call handleProviderResponse with the requestId + try { - provider.handleMutation(mutationsList, observer, currentRequestId, handleProviderResponse); + provider.handleMutation(mutationsList, observer, currentRequestId, handleProviderResponse); } catch (e) { - console.error(CS_LOG_PREFIX, "Error in provider.handleMutation:", e); + console.error(CS_LOG_PREFIX, "Error in provider.handleMutation:", e); } }; const observer = new MutationObserver(callback); - + try { - observer.observe(targetNode, config); - console.log(CS_LOG_PREFIX, "MutationObserver started on:", targetNode); + observer.observe(targetNode, config); + console.log(CS_LOG_PREFIX, "MutationObserver started on:", targetNode); } catch (e) { - console.error(CS_LOG_PREFIX, "Failed to start MutationObserver:", e, "on target:", targetNode); - // Fallback or error handling if observer cannot be started + console.error(CS_LOG_PREFIX, "Failed to start MutationObserver:", e, "on target:", targetNode); } - // Store the observer if we need to disconnect it later - // e.g., window.chatRelayObserver = observer; } -// Function to monitor for the completion of a response (e.g., when a "thinking" indicator disappears) -// This is a more generic version, specific providers might have more tailored logic. function monitorResponseCompletion(element) { if (!provider || !provider.thinkingIndicatorSelector) { console.warn(CS_LOG_PREFIX, "Cannot monitor response completion: No thinkingIndicatorSelector in provider."); @@ -413,35 +360,20 @@ function monitorResponseCompletion(element) { const thinkingIndicator = document.querySelector(provider.thinkingIndicatorSelector); if (!thinkingIndicator) { - // If the indicator is already gone, assume completion or it never appeared. - // Provider's getResponseText should ideally capture the full text. console.log(CS_LOG_PREFIX, "Thinking indicator not found, assuming response is complete or was never present."); - // Potentially call captureResponse one last time if needed by provider logic - // captureResponse(null, true); // Example, might need adjustment return; } console.log(CS_LOG_PREFIX, "Thinking indicator found. Monitoring for its removal..."); const observer = new MutationObserver((mutationsList, obs) => { - // Check if the thinking indicator (or its parent, if it's removed directly) is no longer in the DOM - // or if a specific class/attribute indicating completion appears. - // This logic needs to be robust and provider-specific. - - // A simple check: if the element itself is removed or a known parent. - // More complex checks might involve looking for specific classes on the response element. + if (!document.body.contains(thinkingIndicator)) { console.log(CS_LOG_PREFIX, "Thinking indicator removed. Assuming response completion."); obs.disconnect(); - // Capture the final response - // This assumes captureResponse can get the full text now. - // The 'true' flag indicates this is considered the final capture. - captureResponse(null, true); + captureResponse(null, true); } - // Add other provider-specific checks here if needed }); - // Observe the parent of the thinking indicator for changes in its children (e.g., removal of the indicator) - // Or observe attributes of the indicator itself if it changes state instead of being removed. if (thinkingIndicator.parentNode) { observer.observe(thinkingIndicator.parentNode, { childList: true, subtree: true }); } else { @@ -449,141 +381,116 @@ function monitorResponseCompletion(element) { } } -// Specific monitoring for Gemini, if needed (example) function monitorGeminiResponse(element) { - // Gemini specific logic for monitoring response element for completion - // This might involve looking for specific attributes or child elements - // that indicate the stream has finished. - console.log(CS_LOG_PREFIX, "Monitoring Gemini response element:", element); - // Example: Observe for a specific class or attribute change - const observer = new MutationObserver((mutationsList, obs) => { - let isComplete = false; - // Check mutations for signs of completion based on Gemini's DOM structure - // For instance, a "generating" class is removed, or a "complete" attribute is set. - // This is highly dependent on the actual Gemini interface. - // Example (conceptual): - // if (element.classList.contains('response-complete')) { - // isComplete = true; - // } - - if (isComplete) { - console.log(CS_LOG_PREFIX, "Gemini response detected as complete by mutation."); - obs.disconnect(); - captureResponse(element, true); // Capture final response - } - }); - observer.observe(element, { attributes: true, childList: true, subtree: true }); - console.log(CS_LOG_PREFIX, "Gemini response observer started."); + console.log(CS_LOG_PREFIX, "Monitoring Gemini response element:", element); + const observer = new MutationObserver((mutationsList, obs) => { + let isComplete = false; + + if (isComplete) { + console.log(CS_LOG_PREFIX, "Gemini response detected as complete by mutation."); + obs.disconnect(); + captureResponse(element, true); + } + }); + observer.observe(element, { attributes: true, childList: true, subtree: true }); + console.log(CS_LOG_PREFIX, "Gemini response observer started."); } function monitorGeminiContentStability(element) { - let lastContent = ""; - let stableCount = 0; - const STABLE_THRESHOLD = 3; // Number of intervals content must remain unchanged - const CHECK_INTERVAL = 300; // Milliseconds - - console.log(CS_LOG_PREFIX, "Starting Gemini content stability monitoring for element:", element); - - const intervalId = setInterval(() => { - if (!processingMessage || currentRequestId === null) { - console.log(CS_LOG_PREFIX, "Gemini stability: Stopping, no longer processing message."); - clearInterval(intervalId); - return; - } + let lastContent = ""; + let stableCount = 0; + const STABLE_THRESHOLD = 3; + const CHECK_INTERVAL = 300; - const currentContent = provider.getResponseText(element); - if (currentContent === lastContent) { - stableCount++; - console.log(CS_LOG_PREFIX, `Gemini stability: Content stable, count: ${stableCount}`); - } else { - lastContent = currentContent; - stableCount = 0; // Reset if content changes - console.log(CS_LOG_PREFIX, `Gemini stability: Content changed. New length: ${currentContent.length}`); - // Send partial update if provider wants it - if (provider.sendPartialUpdates) { - handleProviderResponse(currentRequestId, currentContent, false); - } - } + console.log(CS_LOG_PREFIX, "Starting Gemini content stability monitoring for element:", element); - if (stableCount >= STABLE_THRESHOLD) { - console.log(CS_LOG_PREFIX, "Gemini stability: Content stable for threshold. Assuming final."); - clearInterval(intervalId); - // Ensure the very latest content is captured and sent as final - const finalContent = provider.getResponseText(element); - handleProviderResponse(currentRequestId, finalContent, true); - } - }, CHECK_INTERVAL); - responseMonitoringTimers.push(intervalId); // Store to clear if needed + const intervalId = setInterval(() => { + if (!processingMessage || currentRequestId === null) { + console.log(CS_LOG_PREFIX, "Gemini stability: Stopping, no longer processing message."); + clearInterval(intervalId); + return; + } + + const currentContent = provider.getResponseText(element); + if (currentContent === lastContent) { + stableCount++; + console.log(CS_LOG_PREFIX, `Gemini stability: Content stable, count: ${stableCount}`); + } else { + lastContent = currentContent; + stableCount = 0; + console.log(CS_LOG_PREFIX, `Gemini stability: Content changed. New length: ${currentContent.length}`); + if (provider.sendPartialUpdates) { + handleProviderResponse(currentRequestId, currentContent, false); + } + } + + if (stableCount >= STABLE_THRESHOLD) { + console.log(CS_LOG_PREFIX, "Gemini stability: Content stable for threshold. Assuming final."); + clearInterval(intervalId); + const finalContent = provider.getResponseText(element); + handleProviderResponse(currentRequestId, finalContent, true); + } + }, CHECK_INTERVAL); + responseMonitoringTimers.push(intervalId); } -// Function to capture the response text -// potentialTurnElement is passed by some providers (like Gemini) if they identify the specific response "turn" element function captureResponse(potentialTurnElement = null, isFinal = false) { if (!provider || !provider.getResponseText) { console.error(CS_LOG_PREFIX, "Cannot capture response: No provider or getResponseText method."); if (currentRequestId !== null) { - handleProviderResponse(currentRequestId, "Error: Provider misconfiguration for response capture.", true); + handleProviderResponse(currentRequestId, "Error: Provider misconfiguration for response capture.", true); } return; } - // Use the potentialTurnElement if provided and valid, otherwise fall back to provider.responseSelector let responseElement = null; if (potentialTurnElement && typeof potentialTurnElement === 'object' && potentialTurnElement.nodeType === 1) { - responseElement = potentialTurnElement; - console.log(CS_LOG_PREFIX, "Using provided potentialTurnElement for capture:", responseElement); + responseElement = potentialTurnElement; + console.log(CS_LOG_PREFIX, "Using provided potentialTurnElement for capture:", responseElement); } else { - if (!provider.responseSelector) { - console.error(CS_LOG_PREFIX, "Cannot capture response: No responseSelector in provider and no valid potentialTurnElement given."); - if (currentRequestId !== null) { - handleProviderResponse(currentRequestId, "Error: Provider responseSelector missing.", true); - } - return; + if (!provider.responseSelector) { + console.error(CS_LOG_PREFIX, "Cannot capture response: No responseSelector in provider and no valid potentialTurnElement given."); + if (currentRequestId !== null) { + handleProviderResponse(currentRequestId, "Error: Provider responseSelector missing.", true); } - responseElement = document.querySelector(provider.responseSelector); - console.log(CS_LOG_PREFIX, "Using provider.responseSelector for capture:", provider.responseSelector); + return; + } + responseElement = document.querySelector(provider.responseSelector); + console.log(CS_LOG_PREFIX, "Using provider.responseSelector for capture:", provider.responseSelector); } if (!responseElement) { console.warn(CS_LOG_PREFIX, "Response element not found during capture."); - // If it's supposed to be final and element is not found, it might be an issue. if (isFinal && currentRequestId !== null) { - handleProviderResponse(currentRequestId, "Error: Response element not found for final capture.", true); + handleProviderResponse(currentRequestId, "Error: Response element not found for final capture.", true); } return; } const responseText = provider.getResponseText(responseElement); - // isFinal flag is now passed as an argument, but provider might have its own check const trulyFinal = isFinal || (provider.isResponseComplete ? provider.isResponseComplete(responseElement) : false); console.log(CS_LOG_PREFIX, `Captured response (length: ${responseText.length}), isFinal: ${trulyFinal}. Passed isFinal: ${isFinal}`); - + if (currentRequestId === null) { - console.warn(CS_LOG_PREFIX, "captureResponse: currentRequestId is null. Cannot send response to background."); - return; + console.warn(CS_LOG_PREFIX, "captureResponse: currentRequestId is null. Cannot send response to background."); + return; } - // Call handleProviderResponse, which will then relay to background - // This centralizes the logic for sending FINAL_RESPONSE_TO_RELAY handleProviderResponse(currentRequestId, responseText, trulyFinal); } -// Function to clear all active response monitoring timers function clearResponseMonitoringTimers() { - console.log(CS_LOG_PREFIX, `Clearing ${responseMonitoringTimers.length} response monitoring timers.`); - responseMonitoringTimers.forEach(timerId => clearTimeout(timerId)); // Works for both setTimeout and setInterval IDs - responseMonitoringTimers = []; // Reset the array + console.log(CS_LOG_PREFIX, `Clearing ${responseMonitoringTimers.length} response monitoring timers.`); + responseMonitoringTimers.forEach(timerId => clearTimeout(timerId)); + responseMonitoringTimers = []; } -// Define message listener function *before* calling it -// Renamed setupAutomaticMessageSending to setupMessageListeners -function setupMessageListeners() { // Renamed from setupAutomaticMessageSending - // Listen for commands from the background script +function setupMessageListeners() { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "SEND_CHAT_MESSAGE") { - const messageContent = message.messageContent; // Use messageContent + const messageContent = message.messageContent; let messagePreview = ""; if (typeof messageContent === 'string') { messagePreview = `String: "${messageContent.substring(0, 50)}..."`; @@ -594,9 +501,9 @@ function setupMessageListeners() { // Renamed from setupAutomaticMessageSending } else if (messageContent && typeof messageContent === 'object' && messageContent !== null) { messagePreview = `Object data (type: ${Object.prototype.toString.call(messageContent)})`; } else { - messagePreview = `Data type: ${typeof messageContent}, Value: ${String(messageContent).substring(0,50)}`; + messagePreview = `Data type: ${typeof messageContent}, Value: ${String(messageContent).substring(0, 50)}`; } - console.log(CS_LOG_PREFIX, "Received command to send message:", messagePreview, "Request ID:", message.requestId, "Last Processed Text:", message.lastProcessedText ? `"${message.lastProcessedText.substring(0,50)}..."` : "null"); + console.log(CS_LOG_PREFIX, "Received command to send message:", messagePreview, "Request ID:", message.requestId, "Last Processed Text:", message.lastProcessedText ? `"${message.lastProcessedText.substring(0, 50)}..."` : "null"); if (!provider) { console.error(CS_LOG_PREFIX, "Cannot send message: No provider detected."); @@ -604,7 +511,6 @@ function setupMessageListeners() { // Renamed from setupAutomaticMessageSending return true; } - // Superseding / duplicate requestId logic (unchanged) if (processingMessage && currentRequestId !== null && currentRequestId !== message.requestId) { console.warn(CS_LOG_PREFIX, `New message (requestId: ${message.requestId}) received while request ${currentRequestId} was processing. The new message will supersede the old one.`); clearResponseMonitoringTimers(); @@ -612,11 +518,10 @@ function setupMessageListeners() { // Renamed from setupAutomaticMessageSending currentRequestId = null; } else if (processingMessage && currentRequestId === message.requestId) { console.warn(CS_LOG_PREFIX, `Received duplicate SEND_CHAT_MESSAGE for already processing requestId: ${message.requestId}. Ignoring duplicate command.`); - sendResponse({ success: false, error: "Duplicate command for already processing requestId."}); + sendResponse({ success: false, error: "Duplicate command for already processing requestId." }); return true; } - // Attempt to get the input field const inputField = document.querySelector(provider.inputSelector); let currentUIInputText = null; @@ -624,24 +529,18 @@ function setupMessageListeners() { // Renamed from setupAutomaticMessageSending currentUIInputText = inputField.value; } else { console.error(CS_LOG_PREFIX, "Input field not found via selector:", provider.inputSelector, "Cannot process SEND_CHAT_MESSAGE for requestId:", message.requestId); - // Reset state if this was meant to be the current request - if (currentRequestId === message.requestId) { // Check if we were about to set this as current - processingMessage = false; // Ensure it's reset if it was about to become active - // currentRequestId is not yet set to message.requestId here if it's a new command + if (currentRequestId === message.requestId) { + processingMessage = false; } sendResponse({ success: false, error: "Input field not found by content script." }); return true; } - // Duplicate Message Scenario Check: - // 1. We have a record of the last processed text from the background script. - // 2. The server is trying to send that exact same text again (messageContent === message.lastProcessedText). - // 3. The UI input field also currently contains that exact same text (currentUIInputText === messageContent). let isDuplicateMessageScenario = false; if (typeof messageContent === 'string' && typeof message.lastProcessedText === 'string' && typeof currentUIInputText === 'string') { isDuplicateMessageScenario = message.lastProcessedText && - messageContent === message.lastProcessedText && - currentUIInputText === messageContent; + messageContent === message.lastProcessedText && + currentUIInputText === messageContent; } if (isDuplicateMessageScenario) { @@ -649,16 +548,14 @@ function setupMessageListeners() { // Renamed from setupAutomaticMessageSending console.log(CS_LOG_PREFIX, ` Server wants to send: "${messageContent.substring(0, 50)}..."`); console.log(CS_LOG_PREFIX, ` Last processed text was: "${message.lastProcessedText.substring(0, 50)}..."`); console.log(CS_LOG_PREFIX, ` Current UI input is: "${currentUIInputText.substring(0, 50)}..."`); - + console.log(CS_LOG_PREFIX, "Clearing input field and notifying background."); - inputField.value = ''; // Clear the input field - // Optionally, dispatch 'input' or 'change' events if the website needs them for reactivity - // inputField.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); + inputField.value = ''; chrome.runtime.sendMessage({ type: "DUPLICATE_MESSAGE_HANDLED", requestId: message.requestId, - originalText: messageContent // The text that was duplicated + originalText: messageContent }, response => { if (chrome.runtime.lastError) { console.error(CS_LOG_PREFIX, 'Error sending DUPLICATE_MESSAGE_HANDLED:', chrome.runtime.lastError.message); @@ -667,29 +564,22 @@ function setupMessageListeners() { // Renamed from setupAutomaticMessageSending } }); - // This request is now considered "handled" by the content script (as a duplicate). - // Reset content script's immediate processing state if this was about to become the active request. - // Note: currentRequestId might not yet be message.requestId if this is a brand new command. - // The background script will manage its own processingRequest flag based on DUPLICATE_MESSAGE_HANDLED. - // For content.js, we ensure we don't proceed to send this. - // If currentRequestId was already message.requestId (e.g. from a retry/glitch), reset it. if (currentRequestId === message.requestId) { - processingMessage = false; - currentRequestId = null; + processingMessage = false; + currentRequestId = null; } - + sendResponse({ success: true, message: "Duplicate message scenario handled by clearing input." }); return true; } - // If not a duplicate, proceed with normal sending logic: console.log(CS_LOG_PREFIX, `Not a duplicate scenario for requestId: ${message.requestId}. Proceeding to send.`); processingMessage = true; currentRequestId = message.requestId; console.log(CS_LOG_PREFIX, `Set currentRequestId to ${currentRequestId} for processing.`); if (provider && typeof provider.sendChatMessage === 'function') { - provider.sendChatMessage(messageContent, currentRequestId) // Pass messageContent and the requestId + provider.sendChatMessage(messageContent, currentRequestId) .then(success => { if (success) { console.log(CS_LOG_PREFIX, `Message sending initiated successfully via provider for requestId: ${currentRequestId}.`); @@ -698,15 +588,13 @@ function setupMessageListeners() { // Renamed from setupAutomaticMessageSending provider.initiateResponseCapture(currentRequestId, handleProviderResponse); } else { console.error(CS_LOG_PREFIX, `Provider ${provider.name} does not have initiateResponseCapture method. Response will not be processed for requestId ${currentRequestId}.`); - // If no response capture, this request might hang on the server side. - // Consider sending an error back to background.js or directly to server. - chrome.runtime.sendMessage({ - type: "FINAL_RESPONSE_TO_RELAY", - requestId: currentRequestId, - error: `Provider ${provider.name} cannot capture responses. Message sent but no response will be relayed.`, - isFinal: true + chrome.runtime.sendMessage({ + type: "FINAL_RESPONSE_TO_RELAY", + requestId: currentRequestId, + error: `Provider ${provider.name} cannot capture responses. Message sent but no response will be relayed.`, + isFinal: true }); - processingMessage = false; // As we can't process response + processingMessage = false; currentRequestId = null; } sendResponse({ success: true, message: "Message sending initiated by provider." }); @@ -725,41 +613,34 @@ function setupMessageListeners() { // Renamed from setupAutomaticMessageSending } else { console.error(CS_LOG_PREFIX, "Provider or provider.sendChatMessage is not available for requestId:", message.requestId); processingMessage = false; - currentRequestId = null; // Ensure reset if it was about to be set + currentRequestId = null; sendResponse({ success: false, error: "Provider or sendChatMessage method missing." }); } - return true; // Indicate async response + return true; } else if (message.type === "DEBUGGER_RESPONSE") { - console.log(CS_LOG_PREFIX, "Received DEBUGGER_RESPONSE message object:", JSON.stringify(message)); // Log full received message + console.log(CS_LOG_PREFIX, "Received DEBUGGER_RESPONSE message object:", JSON.stringify(message)); console.log(CS_LOG_PREFIX, `Processing DEBUGGER_RESPONSE for app requestId: ${currentRequestId}. Debugger requestId: ${message.requestId}. Data length: ${message.data ? message.data.length : 'null'}`); if (!provider) { - console.error(CS_LOG_PREFIX, "Received DEBUGGER_RESPONSE but no provider is active."); - sendResponse({ success: false, error: "No provider active." }); - return true; + console.error(CS_LOG_PREFIX, "Received DEBUGGER_RESPONSE but no provider is active."); + sendResponse({ success: false, error: "No provider active." }); + return true; } if (typeof provider.handleDebuggerData !== 'function') { - console.error(CS_LOG_PREFIX, `Provider ${provider.name} does not implement handleDebuggerData.`); - sendResponse({ success: false, error: `Provider ${provider.name} does not support debugger method.` }); - return true; + console.error(CS_LOG_PREFIX, `Provider ${provider.name} does not implement handleDebuggerData.`); + sendResponse({ success: false, error: `Provider ${provider.name} does not support debugger method.` }); + return true; } - // IMPORTANT: The message.requestId IS the application's original requestId, - // associated by background.js. We should use this directly. - // The content.js currentRequestId might have been cleared if the provider.sendChatMessage failed, - // but the debugger stream might still be valid for message.requestId. - - if (!message.requestId && message.requestId !== 0) { // Check if message.requestId is missing or invalid (0 is a valid requestId) - console.error(CS_LOG_PREFIX, `Received DEBUGGER_RESPONSE without a valid message.requestId. Ignoring. Message:`, message); - sendResponse({ success: false, error: "DEBUGGER_RESPONSE missing requestId." }); - return true; + + if (!message.requestId && message.requestId !== 0) { + console.error(CS_LOG_PREFIX, `Received DEBUGGER_RESPONSE without a valid message.requestId. Ignoring. Message:`, message); + sendResponse({ success: false, error: "DEBUGGER_RESPONSE missing requestId." }); + return true; } - // Pass the raw data, the message's requestId, and isFinal flag to the provider - // The provider's handleDebuggerData is responsible for calling handleProviderResponse - console.log(CS_LOG_PREFIX, `Calling provider.handleDebuggerData for requestId: ${message.requestId} with isFinal: ${message.isFinal}`); // Log before call + console.log(CS_LOG_PREFIX, `Calling provider.handleDebuggerData for requestId: ${message.requestId} with isFinal: ${message.isFinal}`); provider.handleDebuggerData(message.requestId, message.data, message.isFinal, handleProviderResponse); - // Acknowledge receipt of the debugger data sendResponse({ success: true, message: "Debugger data passed to provider." }); - return true; // Indicate async response (provider will eventually call handleProviderResponse) + return true; } else if (message.type === "PING_TAB") { console.log(CS_LOG_PREFIX, "Received PING_TAB from background script."); @@ -769,13 +650,11 @@ function setupMessageListeners() { // Renamed from setupAutomaticMessageSending console.log(CS_LOG_PREFIX, `Received STOP_STREAMING command for requestId: ${message.requestId}`); if (provider && typeof provider.stopStreaming === 'function') { provider.stopStreaming(message.requestId); - // The handleProviderResponse might have already cleared currentRequestId if it matched. - // We ensure processingMessage is false if this was the active request. if (currentRequestId === message.requestId) { - processingMessage = false; - currentRequestId = null; // Explicitly clear here as well - clearResponseMonitoringTimers(); // Ensure any DOM timers are also cleared - console.log(CS_LOG_PREFIX, `STOP_STREAMING: Cleared active currentRequestId ${message.requestId} and processingMessage flag.`); + processingMessage = false; + currentRequestId = null; + clearResponseMonitoringTimers(); + console.log(CS_LOG_PREFIX, `STOP_STREAMING: Cleared active currentRequestId ${message.requestId} and processingMessage flag.`); } sendResponse({ success: true, message: `Streaming stopped for requestId: ${message.requestId}` }); } else { @@ -785,79 +664,69 @@ function setupMessageListeners() { // Renamed from setupAutomaticMessageSending return true; } - // Handle other potential message types if needed - // else if (message.type === '...') { ... } - // If the message type isn't handled, return false or undefined console.log(CS_LOG_PREFIX, "Unhandled message type received:", message.type || message.action); - // sendResponse({ success: false, error: "Unhandled message type" }); // Optional: send error back - // return false; // Or let it be undefined }); } -// Generic callback function passed to the provider. -// The provider calls this when it has determined the final response or a chunk of it. function handleProviderResponse(requestId, responseText, isFinal) { - console.log(CS_LOG_PREFIX, `handleProviderResponse called for requestId: ${requestId}. Data length: ${responseText ? String(responseText).length : 'null'}. isFinal: ${isFinal}. Data (first 100 chars): '${(responseText || "").substring(0,100)}', Type: ${typeof responseText}`); - - // The requestId parameter here is the one that the provider determined this response is for. - // This should be the definitive requestId for this piece of data. - // We log if content.js's currentRequestId is different, but proceed with the passed 'requestId'. - if (currentRequestId !== requestId && currentRequestId !== null) { // also check currentRequestId is not null to avoid warning on initial load or after reset - console.warn(CS_LOG_PREFIX, `handleProviderResponse: content.js currentRequestId (${currentRequestId}) differs from provider's response requestId (${requestId}). Proceeding with provider's requestId for data relay.`); + console.log(CS_LOG_PREFIX, `handleProviderResponse called for requestId: ${requestId}. Data length: ${responseText ? String(responseText).length : 'null'}. isFinal: ${isFinal}. Data (first 100 chars): '${(responseText || "").substring(0, 100)}', Type: ${typeof responseText}`); + + if (currentRequestId !== requestId && currentRequestId !== null) { + console.warn(CS_LOG_PREFIX, `handleProviderResponse: content.js currentRequestId (${currentRequestId}) differs from provider's response requestId (${requestId}). Proceeding with provider's requestId for data relay.`); } - // Continue to process with the 'requestId' passed to this function. if (chrome.runtime && chrome.runtime.sendMessage) { - const MAX_RESPONSE_TEXT_LENGTH = 500 * 1024; // 500KB limit for safety - let messageToSendToBackground; - - if (responseText && typeof responseText === 'string' && responseText.length > MAX_RESPONSE_TEXT_LENGTH) { - console.warn(CS_LOG_PREFIX, `ResponseText for requestId ${requestId} is too large (${responseText.length} bytes). Sending error and truncated text.`); - messageToSendToBackground = { - type: "FINAL_RESPONSE_TO_RELAY", - requestId: requestId, - error: `Response too large to transmit (length: ${responseText.length}). Check content script logs for truncated version.`, - // text: responseText.substring(0, MAX_RESPONSE_TEXT_LENGTH) + "... (truncated by content.js)", // Optionally send truncated - text: `Error: Response too large (length: ${responseText.length}). See AI Studio for full response.`, // Simpler error text - isFinal: true // This is a final error state - }; - } else { - messageToSendToBackground = { - type: "FINAL_RESPONSE_TO_RELAY", - requestId: requestId, - text: responseText, // Can be null if AIStudioProvider parsed it as such - isFinal: isFinal - }; - } + const MAX_RESPONSE_TEXT_LENGTH = 500 * 1024; + let messageToSendToBackground; - console.log(CS_LOG_PREFIX, `[REQ-${requestId}] PRE-SEND to BG: Type: ${messageToSendToBackground.type}, isFinal: ${messageToSendToBackground.isFinal}, HasError: ${!!messageToSendToBackground.error}, TextLength: ${messageToSendToBackground.text ? String(messageToSendToBackground.text).length : (messageToSendToBackground.error ? String(messageToSendToBackground.error).length : 'N/A')}`); - try { - chrome.runtime.sendMessage(messageToSendToBackground, response => { - if (chrome.runtime.lastError) { - console.error(CS_LOG_PREFIX, `[REQ-${requestId}] SEND FAILED to BG: ${chrome.runtime.lastError.message}. Message attempted:`, JSON.stringify(messageToSendToBackground).substring(0, 500)); - } else { - console.log(CS_LOG_PREFIX, `[REQ-${requestId}] SEND SUCCESS to BG. Ack from BG:`, response); - } - }); - } catch (syncError) { - console.error(CS_LOG_PREFIX, `[REQ-${requestId}] SYNC ERROR sending to BG: ${syncError.message}. Message attempted:`, JSON.stringify(messageToSendToBackground).substring(0, 500), syncError); - } + // Encode special Unicode characters before transmission + const encodedText = responseText ? encodeURIComponent(responseText) : ""; + + if (responseText && typeof responseText === 'string' && responseText.length > MAX_RESPONSE_TEXT_LENGTH) { + console.warn(CS_LOG_PREFIX, `ResponseText for requestId ${requestId} is too large (${responseText.length} bytes). Sending error and truncated text.`); + messageToSendToBackground = { + type: "FINAL_RESPONSE_TO_RELAY", + requestId: requestId, + error: `Response too large to transmit (length: ${responseText.length}). Check content script logs for truncated version.`, + text: `Error: Response too large (length: ${responseText.length}). See AI Studio for full response.`, + isFinal: true, + encoded: true + }; + } else { + messageToSendToBackground = { + type: "FINAL_RESPONSE_TO_RELAY", + requestId: requestId, + text: encodedText, + isFinal: isFinal, + encoded: true + }; + } + + console.log(CS_LOG_PREFIX, `[REQ-${requestId}] PRE-SEND to BG: Type: ${messageToSendToBackground.type}, isFinal: ${messageToSendToBackground.isFinal}, HasError: ${!!messageToSendToBackground.error}, TextLength: ${messageToSendToBackground.text ? String(messageToSendToBackground.text).length : (messageToSendToBackground.error ? String(messageToSendToBackground.error).length : 'N/A')}`); + try { + chrome.runtime.sendMessage(messageToSendToBackground, response => { + if (chrome.runtime.lastError) { + console.error(CS_LOG_PREFIX, `[REQ-${requestId}] SEND FAILED to BG: ${chrome.runtime.lastError.message}. Message attempted:`, JSON.stringify(messageToSendToBackground).substring(0, 500)); + } else { + console.log(CS_LOG_PREFIX, `[REQ-${requestId}] SEND SUCCESS to BG. Ack from BG:`, response); + } + }); + } catch (syncError) { + console.error(CS_LOG_PREFIX, `[REQ-${requestId}] SYNC ERROR sending to BG: ${syncError.message}. Message attempted:`, JSON.stringify(messageToSendToBackground).substring(0, 500), syncError); + } } else { - console.error(CS_LOG_PREFIX, "Cannot send FINAL_RESPONSE_TO_RELAY, runtime is invalid."); + console.error(CS_LOG_PREFIX, "Cannot send FINAL_RESPONSE_TO_RELAY, runtime is invalid."); } if (isFinal) { - // Reset content script state AFTER sending the final response message, - // but only if the finalized requestId matches what content.js currently considers its active request. if (currentRequestId === requestId) { processingMessage = false; currentRequestId = null; - clearResponseMonitoringTimers(); // Clear any timers associated with this request + clearResponseMonitoringTimers(); console.log(CS_LOG_PREFIX, `Processing finished for active requestId: ${requestId}. State reset in content.js.`); } else { console.log(CS_LOG_PREFIX, `Processing finished for requestId: ${requestId}. This was not the active content.js requestId (${currentRequestId}), so content.js state not altered by this finalization. However, timers for ${requestId} might need explicit cleanup if any were started by it.`); - // If specific timers were associated with 'requestId' (not currentRequestId), they should be cleared by the provider or a more granular timer management. } } else { console.log(CS_LOG_PREFIX, `Partial response processed for requestId: ${requestId}. Awaiting more data or final flag.`); @@ -865,22 +734,20 @@ function handleProviderResponse(requestId, responseText, isFinal) { } -// Call initialization functions -// Ensure DOM is ready for provider detection and DOM manipulations if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", attemptInitialization); + document.addEventListener("DOMContentLoaded", attemptInitialization); } else { - attemptInitialization(); // DOMContentLoaded has already fired + attemptInitialization(); } function attemptInitialization() { - console.log(CS_LOG_PREFIX, "Attempting initialization..."); - if (window.attemptedInitialization) { - console.log(CS_LOG_PREFIX, "Initialization already attempted. Skipping."); - return; - } - window.attemptedInitialization = true; - initializeContentRelay(); // Initialize provider detection, DOM setup, etc. - setupMessageListeners(); // Setup listeners for messages from background script - console.log(CS_LOG_PREFIX, "Initialization attempt complete. Message listeners set up."); + console.log(CS_LOG_PREFIX, "Attempting initialization..."); + if (window.attemptedInitialization) { + console.log(CS_LOG_PREFIX, "Initialization already attempted. Skipping."); + return; + } + window.attemptedInitialization = true; + initializeContentRelay(); + setupMessageListeners(); + console.log(CS_LOG_PREFIX, "Initialization attempt complete. Message listeners set up."); } diff --git a/extension/providers/aistudio.js b/extension/providers/aistudio.js index 18f7448..d4baabc 100644 --- a/extension/providers/aistudio.js +++ b/extension/providers/aistudio.js @@ -1,84 +1,45 @@ -/* - * Chat Relay: Relay for AI Chat Interfaces - * Copyright (C) 2025 Jamison Moore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - */ -// AI Chat Relay - AI Studio Provider class AIStudioProvider { constructor() { - // --- START OF CONFIGURABLE PROPERTIES --- - // Method for response capture: "debugger" or "dom" this.captureMethod = "debugger"; - // URL pattern for debugger to intercept if captureMethod is "debugger". Ensure this is specific. - this.debuggerUrlPattern = "*MakerSuiteService/GenerateContent*"; // VERIFY THIS PATTERN - // Whether to include "thinking" process in the message or just the final answer. - // If true, parseDebuggerResponse returns a JSON string: { "thinking": "...", "answer": "..." } - // If false, parseDebuggerResponse returns a string: "answer" + this.debuggerUrlPattern = "*MakerSuiteService/GenerateContent*"; this.includeThinkingInMessage = false; - // Option to enable AI Studio function calling on load - // ENABLE_AISTUDIO_FUNCTION_CALLING: true or false this.ENABLE_AISTUDIO_FUNCTION_CALLING = true; - // --- END OF CONFIGURABLE PROPERTIES --- - this.name = "AIStudioProvider"; // Updated name + this.name = "AIStudioProvider"; this.supportedDomains = ["aistudio.google.com"]; - - // Selectors for the AI Studio interface + this.inputSelector = 'textarea.textarea, textarea.gmat-body-medium, textarea[aria-label="Type something or pick one from prompt gallery"]'; - - // The send button selector + this.sendButtonSelector = 'button.run-button, button[aria-label="Run"], button.mat-mdc-tooltip-trigger.run-button'; - - // Updated response selectors based on the actual elements + this.responseSelector = '.response-container, .response-text, .model-response, .model-response-container, ms-chat-turn, ms-prompt-chunk, ms-text-chunk, .very-large-text-container, .cmark-node'; - - // Thinking indicator selector + this.thinkingIndicatorSelector = '.thinking-indicator, .loading-indicator, .typing-indicator, .response-loading, loading-indicator'; - // Fallback selectors - this.responseSelectorForDOMFallback = '.response-container, .model-response-text'; // Placeholder, adjust as needed - this.thinkingIndicatorSelectorForDOM = '.thinking-indicator, .spinner'; // Placeholder, adjust as needed - - // Last sent message to avoid capturing it as a response + this.responseSelectorForDOMFallback = '.response-container, .model-response-text'; + this.thinkingIndicatorSelectorForDOM = '.thinking-indicator, .spinner'; + this.lastSentMessage = ''; - // Initialize pendingResponseCallbacks this.pendingResponseCallbacks = new Map(); - // Call the method to ensure function calling is enabled on initial load this.ensureFunctionCallingEnabled(); - // Listen for SPA navigation events to re-trigger the check if (window.navigation) { window.navigation.addEventListener('navigate', (event) => { - // We are interested in same-document navigations, common in SPAs if (!event.canIntercept || event.hashChange || event.downloadRequest !== null) { return; } - // Check if the navigation is within the same origin and path structure of AI Studio const currentUrl = new URL(window.location.href); const destinationUrl = new URL(event.destination.url); if (currentUrl.origin === destinationUrl.origin && destinationUrl.pathname.startsWith("/prompts/")) { console.log(`[${this.name}] Detected SPA navigation to: ${event.destination.url}. Re-checking function calling toggle.`); - // Use a timeout to allow the new view's DOM to settle setTimeout(() => { this.ensureFunctionCallingEnabled(); - }, 1000); // Delay to allow DOM update + }, 1000); } }); } else { @@ -92,18 +53,17 @@ class AIStudioProvider { return; } - const checkInterval = 500; // ms - const maxDuration = 7000; // ms + const checkInterval = 500; + const maxDuration = 7000; let elapsedTime = 0; const providerName = this.name; - // Clear any existing timer for this specific functionality to avoid multiple polling loops if (this.functionCallingPollTimer) { - clearTimeout(this.functionCallingPollTimer); - this.functionCallingPollTimer = null; - console.log(`[${providerName}] Cleared previous function calling poll timer.`); + clearTimeout(this.functionCallingPollTimer); + this.functionCallingPollTimer = null; + console.log(`[${providerName}] Cleared previous function calling poll timer.`); } - + console.log(`[${providerName}] Ensuring function calling is enabled (polling up to ${maxDuration / 1000}s).`); const tryEnableFunctionCalling = () => { @@ -115,7 +75,6 @@ class AIStudioProvider { if (!isChecked) { console.log(`[${providerName}] Function calling toggle found and is NOT checked. Attempting to enable...`); functionCallingToggle.click(); - // Verify after a short delay if the click was successful setTimeout(() => { const stillChecked = functionCallingToggle.getAttribute('aria-checked') === 'true'; if (stillChecked) { @@ -127,7 +86,7 @@ class AIStudioProvider { } else { console.log(`[${providerName}] Function calling toggle found and is already enabled.`); } - this.functionCallingPollTimer = null; // Clear timer once action is taken or element found + this.functionCallingPollTimer = null; } else { elapsedTime += checkInterval; if (elapsedTime < maxDuration) { @@ -135,35 +94,29 @@ class AIStudioProvider { this.functionCallingPollTimer = setTimeout(tryEnableFunctionCalling, checkInterval); } else { console.warn(`[${providerName}] Function calling toggle button (selector: 'button[aria-label="Function calling"]') not found after ${maxDuration}ms. It might not be available on this page/view or selector is incorrect.`); - this.functionCallingPollTimer = null; // Clear timer + this.functionCallingPollTimer = null; } } }; - // Start the first attempt after a brief initial delay this.functionCallingPollTimer = setTimeout(tryEnableFunctionCalling, 500); } - // Send a message to the chat interface - async sendChatMessage(messageContent) { - console.log(`[${this.name}] sendChatMessage called with content type:`, typeof messageContent, Array.isArray(messageContent) ? `Array length: ${messageContent.length}` : ''); + async sendChatMessage(messageContent, requestId) { + console.log(`[${this.name} REQ-${requestId}] sendChatMessage called with content type:`, typeof messageContent, Array.isArray(messageContent) ? `Array length: ${messageContent.length}` : ''); const inputField = document.querySelector(this.inputSelector); - const sendButton = document.querySelector(this.sendButtonSelector); - if (!inputField || !sendButton) { - console.error(`[${this.name}] Missing input field or send button. Input: ${this.inputSelector}, Button: ${this.sendButtonSelector}`); + if (!inputField) { + console.error(`[${this.name} REQ-${requestId}] Missing input field. Input selector: ${this.inputSelector}`); return false; } - console.log(`[${this.name}] Attempting to send message to AI Studio with:`, { - inputField: inputField.className, - sendButton: sendButton.getAttribute('aria-label') || sendButton.className - }); + // Deferred sendButton logging moved to after it's defined try { let textToInput = ""; let blobToPaste = null; - let blobMimeType = "image/png"; // Default MIME type + let blobMimeType = "image/png"; if (typeof messageContent === 'string') { textToInput = messageContent; @@ -181,7 +134,7 @@ class AIStudioProvider { textToInput += (textToInput ? "\n" : "") + part.text; console.log(`[${this.name}] Added text part:`, part.text.substring(0, 50) + "..."); } else if (part.type === "image_url" && part.image_url && typeof part.image_url.url === 'string') { - if (!blobToPaste) { // Prioritize the first image found + if (!blobToPaste) { try { const response = await fetch(part.image_url.url); blobToPaste = await response.blob(); @@ -195,25 +148,20 @@ class AIStudioProvider { } } } - this.lastSentMessage = `Array content (Text: "${textToInput.substring(0,50)}...", Image: ${blobToPaste ? 'Yes' : 'No'})`; + this.lastSentMessage = `Array content (Text: "${textToInput.substring(0, 50)}...", Image: ${blobToPaste ? 'Yes' : 'No'})`; } else { console.error(`[${this.name}] Unhandled message content type: ${typeof messageContent}. Cannot send.`); this.lastSentMessage = `Unhandled data type: ${typeof messageContent}`; return false; } - // Set text input if any if (textToInput) { inputField.value = textToInput; inputField.dispatchEvent(new Event('input', { bubbles: true })); console.log(`[${this.name}] Set input field value with accumulated text.`); } else { - // If there's no text but an image, ensure the input field is clear if AI Studio requires it - // inputField.value = ""; - // inputField.dispatchEvent(new Event('input', { bubbles: true })); } - // Paste blob if any if (blobToPaste) { const dataTransfer = new DataTransfer(); const file = new File([blobToPaste], "pasted_image." + (blobMimeType.split('/')[1] || 'png'), { type: blobMimeType }); @@ -226,50 +174,65 @@ class AIStudioProvider { inputField.dispatchEvent(pasteEvent); console.log(`[${this.name}] Dispatched paste event with Blob data.`); } - + inputField.focus(); await new Promise(resolve => setTimeout(resolve, 100)); let attempts = 0; - const maxAttempts = 60; // Try up to 60 times (5 minutes total) - const retryDelay = 5000; // 5 seconds delay between attempts + const maxAttempts = 60; + const retryDelay = 5000; while (attempts < maxAttempts) { + const sendButton = document.querySelector(this.sendButtonSelector); + if (attempts === 0) { + console.log(`[${this.name} REQ-${requestId}] Initial attempt to send. Input field class: ${inputField.className}. Send button details:`, + sendButton ? { + ariaLabel: sendButton.getAttribute('aria-label'), + className: sendButton.className, + selector: this.sendButtonSelector + } : "NOT FOUND"); + } + if (!sendButton) { + console.warn(`[${this.name} REQ-${requestId}] Send button not found (attempt ${attempts + 1}). Will retry.`); + attempts++; + if (attempts >= maxAttempts) { + console.error(`[${this.name} REQ-${requestId}] Send button not found after ${maxAttempts} attempts.`); + return false; + } + await new Promise(resolve => setTimeout(resolve, retryDelay)); + continue; + } + const isDisabled = sendButton.disabled || - sendButton.getAttribute('aria-disabled') === 'true' || - sendButton.classList.contains('disabled'); + sendButton.getAttribute('aria-disabled') === 'true' || + sendButton.classList.contains('disabled'); if (!isDisabled) { - // Removed check for input field content matching lastSentMessage - // as it can cause issues when there are multiple messages waiting to be sent - console.log(`[${this.name}] Send button is enabled. Clicking send button (attempt ${attempts + 1}).`); + console.log(`[${this.name} REQ-${requestId}] Send button is enabled. Clicking send button (attempt ${attempts + 1}).`); sendButton.click(); - return true; // Successfully clicked + return true; } attempts++; if (attempts >= maxAttempts) { console.error(`[${this.name}] Send button remained disabled after ${maxAttempts} attempts. Failed to send message.`); - return false; // Failed to send + return false; } console.log(`[${this.name}] Send button is disabled (attempt ${attempts}). Trying to enable and will retry in ${retryDelay}ms.`); - // Attempt to trigger UI updates that might enable the button - inputField.dispatchEvent(new Event('input', { bubbles: true })); // Re-dispatch input + inputField.dispatchEvent(new Event('input', { bubbles: true })); inputField.dispatchEvent(new Event('change', { bubbles: true })); inputField.dispatchEvent(new Event('blur', { bubbles: true })); - // Focusing and bluring input sometimes helps enable send buttons inputField.focus(); - await new Promise(resolve => setTimeout(resolve, 50)); // Short delay for focus + await new Promise(resolve => setTimeout(resolve, 50)); inputField.blur(); - + await new Promise(resolve => setTimeout(resolve, retryDelay)); } - // Should not be reached if logic is correct, but as a fallback: console.error(`[${this.name}] Exited send button check loop unexpectedly.`); return false; } catch (error) { - console.error(`[${this.name}] Error sending message to AI Studio:`, error); + console.error(`[${this.name} REQ-${requestId}] Error sending message to AI Studio:`, error); return false; } } @@ -282,16 +245,16 @@ class AIStudioProvider { } else if (this.captureMethod === "dom") { console.log(`[${this.name}] Starting DOM monitoring for requestId: ${requestId}`); this.pendingResponseCallbacks.set(requestId, responseCallback); - this._stopDOMMonitoring(); - this._startDOMMonitoring(requestId); + this._stopDOMMonitoring(); + this._startDOMMonitoring(requestId); } else { console.error(`[${this.name}] Unknown capture method: ${this.captureMethod}`); - responseCallback(requestId, `[Error: Unknown capture method '${this.captureMethod}' in provider]`, true); - this.pendingResponseCallbacks.delete(requestId); + responseCallback(requestId, `[Error: Unknown capture method '${this.captureMethod}' in provider]`, true); + this.pendingResponseCallbacks.delete(requestId); } } - handleDebuggerData(requestId, rawData, isFinalFromBackground) { // Renamed isFinal to isFinalFromBackground for clarity + handleDebuggerData(requestId, rawData, isFinalFromBackground) { console.log(`[${this.name}] handleDebuggerData called for requestId: ${requestId}. Raw data length: ${rawData ? rawData.length : 'null'}. isFinalFromBackground: ${isFinalFromBackground}`); const callback = this.pendingResponseCallbacks.get(requestId); if (!callback) { @@ -303,48 +266,41 @@ class AIStudioProvider { let contentHasInternalFinalMarker = false; if (rawData && rawData.trim() !== "") { - const parseOutput = this.parseDebuggerResponse(rawData); - parsedText = parseOutput.text; - contentHasInternalFinalMarker = parseOutput.isFinalResponse; // Use the parser's determination - console.log(`[${this.name}] Debugger data parsed for requestId: ${requestId}. Parsed text (first 100 chars): '${(parsedText || "").substring(0,100)}', Type: ${typeof parsedText}, ChunkHasFinalMarkerFromParser: ${contentHasInternalFinalMarker}`); + const parseOutput = this.parseDebuggerResponse(rawData); + parsedText = parseOutput.text; + contentHasInternalFinalMarker = parseOutput.isFinalResponse; + console.log(`[${this.name}] Debugger data parsed for requestId: ${requestId}. Parsed text (first 100 chars): '${(parsedText || "").substring(0, 100)}', Type: ${typeof parsedText}, ChunkHasFinalMarkerFromParser: ${contentHasInternalFinalMarker}`); } else { console.log(`[${this.name}] Received empty rawData from debugger for requestId: ${requestId}. isFinalFromBackground: ${isFinalFromBackground}`); - // If rawData is empty, text remains empty. - // If background says it's final, but data is empty, it's still final. } - - // The response is considered final for the callback if: - // 1. The background script explicitly states this is the final debugger event for the request OR - // 2. The provider's own parsing of the current chunk's content indicates it's the end of the AI's message. + const isFinalForCallback = isFinalFromBackground || contentHasInternalFinalMarker; - console.log(`[${this.name}] Calling callback for requestId ${requestId} with text (first 100): '${(parsedText || "").substring(0,100)}', isFinalForCallback: ${isFinalForCallback} (isFinalFromBackground: ${isFinalFromBackground}, contentHasInternalFinalMarker: ${contentHasInternalFinalMarker})`); + console.log(`[${this.name}] Calling callback for requestId ${requestId} with text (first 100): '${(parsedText || "").substring(0, 100)}', isFinalForCallback: ${isFinalForCallback} (isFinalFromBackground: ${isFinalFromBackground}, contentHasInternalFinalMarker: ${contentHasInternalFinalMarker})`); callback(requestId, parsedText, isFinalForCallback); - - // If the callback was told this is the final response, then clean up. + if (isFinalForCallback) { console.log(`[${this.name}] Final event processed for requestId: ${requestId} (isFinalForCallback was true). Removing callback.`); this.pendingResponseCallbacks.delete(requestId); } } - // --- Internal DOM Capture Logic (largely unchanged but kept for completeness) --- - _captureResponseDOM(element = null) { + _captureResponseDOM(element = null) { console.log(`[${this.name}] _captureResponseDOM (DOM method) called with element:`, element); - if (!element && this.captureMethod === "dom") { - const elements = document.querySelectorAll(this.responseSelector); - if (elements.length > 0) { - element = elements[elements.length - 1]; - console.log(`[${this.name}] _captureResponseDOM: Found element via querySelector during polling.`); - } + if (!element && this.captureMethod === "dom") { + const elements = document.querySelectorAll(this.responseSelector); + if (elements.length > 0) { + element = elements[elements.length - 1]; + console.log(`[${this.name}] _captureResponseDOM: Found element via querySelector during polling.`); + } } if (!element) { console.log(`[${this.name}] _captureResponseDOM: No element provided or found.`); - return { found: false, text: '' }; + return { found: false, text: '' }; } - if (this._isResponseStillGeneratingDOM()) { + if (this._isResponseStillGeneratingDOM()) { console.log(`[${this.name}] Response is still being generated (_isResponseStillGeneratingDOM check), waiting for completion`); - return { found: false, text: '' }; + return { found: false, text: '' }; } console.log(`[${this.name}] Attempting to capture DOM response from AI Studio...`); let responseText = ""; @@ -355,10 +311,9 @@ class AIStudioProvider { console.log("AISTUDIO: Element has text content"); responseText = element.textContent.trim(); if (responseText && - // Removed check for responseText !== this.lastSentMessage - !responseText.includes("Loading") && - !responseText.includes("Thinking") && - !responseText.includes("Expand to view model thoughts")) { + !responseText.includes("Loading") && + !responseText.includes("Thinking") && + !responseText.includes("Expand to view model thoughts")) { console.log("AISTUDIO: Found response in element:", responseText.substring(0, 50) + (responseText.length > 50 ? "..." : "")); foundResponse = true; } else { @@ -384,9 +339,8 @@ class AIStudioProvider { if (responseSpan) { console.log("AISTUDIO: Found response span in last ms-text-chunk"); const text = responseSpan.textContent.trim(); - if (text && - // Removed check for text !== this.lastSentMessage - !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { + if (text && + !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { responseText = text; console.log("AISTUDIO: Found response in span:", responseText.substring(0, 50) + (responseText.length > 50 ? "..." : "")); foundResponse = true; @@ -394,9 +348,8 @@ class AIStudioProvider { } else { console.log("AISTUDIO: No response span found, getting text directly from ms-text-chunk"); const text = lastTextChunk.textContent.trim(); - if (text && - // Removed check for text !== this.lastSentMessage - !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { + if (text && + !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { responseText = text; console.log("AISTUDIO: Found response in ms-text-chunk:", responseText.substring(0, 50) + (responseText.length > 50 ? "..." : "")); foundResponse = true; @@ -412,9 +365,8 @@ class AIStudioProvider { const isInThoughtChunk = p.closest('ms-thought-chunk'); if (!isInThoughtChunk) { const text = p.textContent.trim(); - if (text && - // Removed check for text !== this.lastSentMessage - !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { + if (text && + !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { combinedText += text + "\n"; } } @@ -438,9 +390,8 @@ class AIStudioProvider { let combinedTextFallback = ""; paragraphsFallback.forEach((p) => { const text = p.textContent.trim(); - if (text && - // Removed check for text !== this.lastSentMessage - !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { + if (text && + !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { combinedTextFallback += text + "\n"; } }); @@ -451,9 +402,8 @@ class AIStudioProvider { } if (!foundResponse) { const textFallback = lastChatTurnFallback.textContent.trim(); - if (textFallback && - // Removed check for textFallback !== this.lastSentMessage - !textFallback.includes("Loading") && !textFallback.includes("Thinking") && !textFallback.includes("Expand to view model thoughts")) { + if (textFallback && + !textFallback.includes("Loading") && !textFallback.includes("Thinking") && !textFallback.includes("Expand to view model thoughts")) { responseText = textFallback; foundResponse = true; } @@ -467,9 +417,8 @@ class AIStudioProvider { for (let i = textContainers.length - 1; i >= 0; i--) { const textContainer = textContainers[i]; const text = textContainer.textContent.trim(); - if (text && - // Removed check for text !== this.lastSentMessage - !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { + if (text && + !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { responseText = text; foundResponse = true; break; @@ -487,9 +436,8 @@ class AIStudioProvider { const isUserChunk = paragraph.closest('.user-chunk'); if (isUserChunk) continue; const text = paragraph.textContent.trim(); - if (text && - // Removed check for text !== this.lastSentMessage - !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { + if (text && + !text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) { combinedTextDoc = text + "\n" + combinedTextDoc; if (text.startsWith("Hello") || text.includes("I'm doing") || text.includes("How can I assist")) break; } @@ -513,126 +461,123 @@ class AIStudioProvider { .replace(/\n{3,}/g, '\n\n') .trim(); } - return { + return { found: foundResponse && !!responseText.trim(), text: responseText }; } - // --- START OF CORRECTED DEBUGGER PARSING LOGIC --- parseDebuggerResponse(jsonString) { - console.log(`[${this.name}] Parsing debugger response (AI Studio specific)... Input jsonString (first 200):`, jsonString ? jsonString.substring(0,200) : "null", "Type:", typeof jsonString); - + console.log(`[${this.name}] Parsing debugger response (AI Studio specific)... Input jsonString (first 200):`, jsonString ? jsonString.substring(0, 200) : "null", "Type:", typeof jsonString); + if (!jsonString || jsonString.trim() === "") { - console.warn(`[${this.name}] parseDebuggerResponse called with empty or null jsonString.`); - return { text: "", isFinalResponse: false }; + console.warn(`[${this.name}] parseDebuggerResponse called with empty or null jsonString.`); + return { text: "", isFinalResponse: false }; } let thinkingAndProcessText = ""; let actualResponseText = ""; - let overallMarkerFound = false; + let overallMarkerFound = false; function findEndOfUnitMarker(data) { - if (Array.isArray(data)) { - if (data.length >= 2 && data[data.length - 1] === 1 && data[data.length - 2] === "model") { - return true; - } - for (const item of data) { - if (findEndOfUnitMarker(item)) { - return true; - } - } + if (Array.isArray(data)) { + if (data.length >= 2 && data[data.length - 1] === 1 && data[data.length - 2] === "model") { + return true; } - return false; + for (const item of data) { + if (findEndOfUnitMarker(item)) { + return true; + } + } + } + return false; } function extractTextSegments(data, segments = []) { - if (Array.isArray(data)) { - if (data.length > 1 && data[0] === null && typeof data[1] === 'string') { - segments.push(data[1]); - } else { - for (const item of data) { - extractTextSegments(item, segments); - } - } + if (Array.isArray(data)) { + if (data.length > 1 && data[0] === null && typeof data[1] === 'string') { + segments.push(data[1]); + } else { + for (const item of data) { + extractTextSegments(item, segments); + } } - return segments; + } + return segments; } try { - const parsedJson = JSON.parse(jsonString); - if (Array.isArray(parsedJson)) { - for (let i = 0; i < parsedJson.length; i++) { - const chunk = parsedJson[i]; - const textSegmentsInChunk = extractTextSegments(chunk); - if (textSegmentsInChunk.length > 0) { - actualResponseText += textSegmentsInChunk.join(""); - } - if (findEndOfUnitMarker(chunk)) { - overallMarkerFound = true; - } - if (this.includeThinkingInMessage) { - if (Array.isArray(chunk) && chunk[0] && Array.isArray(chunk[0][0]) && chunk[0][0][2]) { - const potentialThinkingBlock = chunk[0][0][2]; - const thinkingSegments = extractTextSegments(potentialThinkingBlock); - const thinkingBlockText = thinkingSegments.join("").trim(); - if (thinkingBlockText && !actualResponseText.includes(thinkingBlockText)) { - thinkingAndProcessText += thinkingBlockText + "\n"; - } - } - } + const parsedJson = JSON.parse(jsonString); + if (Array.isArray(parsedJson)) { + for (let i = 0; i < parsedJson.length; i++) { + const chunk = parsedJson[i]; + const textSegmentsInChunk = extractTextSegments(chunk); + if (textSegmentsInChunk.length > 0) { + actualResponseText += textSegmentsInChunk.join(""); + } + if (findEndOfUnitMarker(chunk)) { + overallMarkerFound = true; + } + if (this.includeThinkingInMessage) { + if (Array.isArray(chunk) && chunk[0] && Array.isArray(chunk[0][0]) && chunk[0][0][2]) { + const potentialThinkingBlock = chunk[0][0][2]; + const thinkingSegments = extractTextSegments(potentialThinkingBlock); + const thinkingBlockText = thinkingSegments.join("").trim(); + if (thinkingBlockText && !actualResponseText.includes(thinkingBlockText)) { + thinkingAndProcessText += thinkingBlockText + "\n"; + } } + } + } + } else { + if (typeof parsedJson === 'string') { + actualResponseText = parsedJson; + overallMarkerFound = true; } else { - if (typeof parsedJson === 'string') { - actualResponseText = parsedJson; - overallMarkerFound = true; - } else { - console.warn(`[${this.name}] Parsed JSON is not an array as expected. Type: ${typeof parsedJson}. Content (first 100): ${JSON.stringify(parsedJson).substring(0,100)}`); - const genericText = extractTextSegments(parsedJson).join(""); - if (genericText) { - actualResponseText = genericText; - overallMarkerFound = true; - } else { - actualResponseText = "[Error: Unexpected JSON structure from AI Studio]"; - overallMarkerFound = true; - } - } + console.warn(`[${this.name}] Parsed JSON is not an array as expected. Type: ${typeof parsedJson}. Content (first 100): ${JSON.stringify(parsedJson).substring(0, 100)}`); + const genericText = extractTextSegments(parsedJson).join(""); + if (genericText) { + actualResponseText = genericText; + overallMarkerFound = true; + } else { + actualResponseText = "[Error: Unexpected JSON structure from AI Studio]"; + overallMarkerFound = true; + } } - - actualResponseText = actualResponseText.replace(/\\n/g, "\n").replace(/\n\s*\n/g, '\n').trim(); - thinkingAndProcessText = thinkingAndProcessText.replace(/\\n/g, "\n").replace(/\n\s*\n/g, '\n').trim(); + } + + actualResponseText = actualResponseText.replace(/\\n/g, "\n").replace(/\n\s*\n/g, '\n').trim(); + thinkingAndProcessText = thinkingAndProcessText.replace(/\\n/g, "\n").replace(/\n\s*\n/g, '\n').trim(); } catch (e) { - console.error(`[${this.name}] Error parsing AI Studio debugger response JSON:`, e, "Original string (first 200 chars):", jsonString.substring(0, 200)); - const formattedFallback = this.formatOutput("", jsonString); - return { text: formattedFallback, isFinalResponse: true }; + console.error(`[${this.name}] Error parsing AI Studio debugger response JSON:`, e, "Original string (first 200 chars):", jsonString.substring(0, 200)); + const formattedFallback = this.formatOutput("", jsonString); + return { text: formattedFallback, isFinalResponse: true }; } - + const formattedOutput = this.formatOutput(thinkingAndProcessText, actualResponseText); if (formattedOutput.trim() === "" && overallMarkerFound) { - return { text: "", isFinalResponse: true }; + return { text: "", isFinalResponse: true }; } return { text: formattedOutput, isFinalResponse: overallMarkerFound }; } formatOutput(thinkingText, answerText) { if (this.includeThinkingInMessage && thinkingText && thinkingText.trim() !== "") { - try { - const result = { - thinking: thinkingText.trim(), - answer: (answerText || "").trim() - }; - return JSON.stringify(result); - } catch (e) { - console.error(`[${this.name}] Error stringifying thinking/answer object:`, e); - return (answerText || "").trim(); - } + try { + const result = { + thinking: thinkingText.trim(), + answer: (answerText || "").trim() + }; + return JSON.stringify(result); + } catch (e) { + console.error(`[${this.name}] Error stringifying thinking/answer object:`, e); + return (answerText || "").trim(); + } } - return (answerText || "").trim(); + return (answerText || "").trim(); } - // --- END OF CORRECTED DEBUGGER PARSING LOGIC --- - - // --- Other methods (DOM fallback, etc. - largely unchanged but included for completeness) --- + _findResponseElementDOM(container) { console.log(`[${this.name}] _findResponseElementDOM called on container:`, container); if (!container) return null; @@ -641,7 +586,6 @@ class AIStudioProvider { if (elements.length > 0) { const lastElement = elements[elements.length - 1]; console.log(`[${this.name}] Found last response element via DOM:`, lastElement); - // Add checks to ensure it's not the user's input or an old response if (lastElement.textContent && lastElement.textContent.trim() !== this.lastSentMessage) { return lastElement; } @@ -651,21 +595,14 @@ class AIStudioProvider { } shouldSkipResponseMonitoring() { - // Example: if a provider indicates via a specific property or method - // For AIStudio, if using debugger, we don't need DOM monitoring. - // This method is more for providers that might sometimes use DOM, sometimes not. - // console.log(`[${this.name}] shouldSkipResponseMonitoring called. Capture method: ${this.captureMethod}`); return this.captureMethod === "debugger"; } _isResponseStillGeneratingDOM() { - // This is for the DOM fallback method const thinkingIndicator = document.querySelector(this.thinkingIndicatorSelectorForDOM); if (thinkingIndicator) { - // console.log(`[${this.name}] DOM Fallback: Thinking indicator found.`); return true; } - // console.log(`[${this.name}] DOM Fallback: No thinking indicator found.`); return false; } @@ -681,7 +618,7 @@ class AIStudioProvider { _startDOMMonitoring(requestId) { console.log(`[${this.name}] DOM Fallback: _startDOMMonitoring for requestId: ${requestId}`); - this._stopDOMMonitoring(); // Stop any existing observer + this._stopDOMMonitoring(); const callback = this.pendingResponseCallbacks.get(requestId); if (!callback) { @@ -690,24 +627,24 @@ class AIStudioProvider { } let attempts = 0; - const maxAttempts = 15; // Try for ~15 seconds + const maxAttempts = 15; const interval = 1000; this.domMonitorTimer = setInterval(() => { console.log(`[${this.name}] DOM Fallback: Polling attempt ${attempts + 1}/${maxAttempts} for requestId: ${requestId}`); - const responseData = this._captureResponseDOM(); // Will use this.responseSelectorForDOMFallback + const responseData = this._captureResponseDOM(); if (responseData.found && responseData.text.trim() !== "") { - console.log(`[${this.name}] DOM Fallback: Response captured for requestId ${requestId}. Text (first 100): ${responseData.text.substring(0,100)}`); + console.log(`[${this.name}] DOM Fallback: Response captured for requestId ${requestId}. Text (first 100): ${responseData.text.substring(0, 100)}`); this._stopDOMMonitoring(); - callback(requestId, responseData.text, true); // Assume final for DOM capture + callback(requestId, responseData.text, true); this.pendingResponseCallbacks.delete(requestId); } else { attempts++; if (attempts >= maxAttempts) { console.warn(`[${this.name}] DOM Fallback: Max attempts reached for requestId ${requestId}. No response captured.`); this._stopDOMMonitoring(); - callback(requestId, "[Error: Timed out waiting for DOM response]", true); // Error, final + callback(requestId, "[Error: Timed out waiting for DOM response]", true); this.pendingResponseCallbacks.delete(requestId); } } @@ -724,7 +661,6 @@ class AIStudioProvider { } } -// Ensure the provider is available on the window for the content script if (window.providerUtils) { const providerInstance = new AIStudioProvider(); window.providerUtils.registerProvider( diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json index 75cab07..07dd57b 100644 --- a/mcp-server/package-lock.json +++ b/mcp-server/package-lock.json @@ -8,6 +8,7 @@ "name": "chat-relay-mcp", "version": "0.0.1", "dependencies": { + "chat-relay-mcp": "file:", "mcp-framework": "^0.2.2", "node-fetch": "^3.3.2", "ws": "^8.18.2", @@ -175,6 +176,10 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chat-relay-mcp": { + "resolved": "", + "link": true + }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", diff --git a/mcp-server/package.json b/mcp-server/package.json index 8b1219d..8a9951b 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -15,6 +15,7 @@ "start": "node dist/index.js" }, "dependencies": { + "chat-relay-mcp": "file:", "mcp-framework": "^0.2.2", "node-fetch": "^3.3.2", "ws": "^8.18.2",