@@ -215,8 +328,13 @@
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') {
+
+ // If settings tab is activated, refresh its content
+ if (link.dataset.tab === 'settings') {
+ fetchAndDisplayServerInfo(); // This function now also updates status on the messages tab
+ }
+ // Always ensure server info (which includes status) is fresh when messages tab is active
+ if (link.dataset.tab === 'messages') {
fetchAndDisplayServerInfo();
}
});
@@ -231,21 +349,23 @@
}
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) {
cell.textContent = 'N/A';
} else {
const pre = document.createElement('pre');
- pre.style.margin = '0';
- pre.style.whiteSpace = 'pre-wrap';
- pre.style.maxHeight = '200px'; // Added max height
- pre.style.overflowY = 'auto'; // Added scrollability
+ // pre.style.margin = '0'; // Handled by td pre CSS
+ // pre.style.whiteSpace = 'pre-wrap'; // Handled by td pre CSS
+ // pre.style.maxHeight = '200px'; // Handled by td pre CSS
+ // pre.style.overflowY = 'auto'; // Handled by td pre CSS
pre.textContent = JSON.stringify(data, null, 2);
cell.appendChild(pre);
}
return cell;
}
+
async function fetchAndDisplayMessageHistory() {
try {
const response = await fetch('/v1/admin/message-history');
@@ -253,7 +373,9 @@
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();
@@ -262,6 +384,7 @@
cell.style.textAlign = 'center';
return;
}
+
// Group messages by requestId
const groupedMessages = messages.reduce((acc, logEntry) => {
const id = logEntry.requestId;
@@ -277,6 +400,7 @@
status: "Unknown"
};
}
+
// Update fields based on log type
switch (logEntry.type) {
case 'CHAT_REQUEST_RECEIVED':
@@ -304,29 +428,35 @@
}
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();
- row.insertCell().textContent = msg.endTimestamp ? new Date(msg.endTimestamp).toLocaleString() : (msg.status === "Request In Progress" ? "In Progress" : "N/A");
- row.insertCell().textContent = msg.requestId;
- row.appendChild(createPreCell(msg.fromClient));
- row.appendChild(createPreCell(msg.toExtension));
- row.appendChild(createPreCell(msg.fromExtension));
- row.appendChild(createPreCell(msg.toClient));
- row.insertCell().textContent = msg.status;
+ let cell;
+ cell = row.insertCell(); cell.textContent = new Date(msg.startTimestamp).toLocaleString(); cell.classList.add('col-timestamp');
+ cell = row.insertCell(); cell.textContent = msg.endTimestamp ? new Date(msg.endTimestamp).toLocaleString() : (msg.status === "Request In Progress" ? "In Progress" : "N/A"); cell.classList.add('col-timestamp');
+ cell = row.insertCell(); cell.textContent = msg.requestId; cell.classList.add('col-request-id');
+
+ cell = createPreCell(msg.fromClient); cell.classList.add('col-data', 'cell-fromClient'); row.appendChild(cell);
+ cell = createPreCell(msg.toExtension); cell.classList.add('col-data', 'cell-toExtension'); row.appendChild(cell);
+ cell = createPreCell(msg.fromExtension); cell.classList.add('col-data', 'cell-fromExtension'); row.appendChild(cell);
+ cell = createPreCell(msg.toClient); cell.classList.add('col-data', 'cell-toClient'); row.appendChild(cell);
+
+ cell = row.insertCell(); cell.textContent = msg.status; cell.classList.add('col-status');
});
} catch (error) {
console.error('Error fetching message history:', error);
@@ -339,16 +469,19 @@
cell.style.textAlign = 'center';
}
}
+
if (refreshButton) {
refreshButton.addEventListener('click', fetchAndDisplayMessageHistory);
}
+
fetchAndDisplayMessageHistory(); // Initial load for messages
+ fetchAndDisplayServerInfo(); // Initial load for server info (status) on the messages tab
+
// 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');
@@ -356,6 +489,7 @@
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));
@@ -364,6 +498,7 @@
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 `;
@@ -371,83 +506,123 @@
uptimeString += `${seconds}s`;
return uptimeString.trim() || '0s';
}
+
async function fetchAndDisplayServerInfo() {
try {
- const response = await fetch('/v1/admin/server-info');
+ // UPDATED: Fetch from the new consolidated settings endpoint
+ const response = await fetch('/admin/settings');
if (!response.ok) {
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 (serverInfo.newRequestBehavior === 'drop') {
- if (newRequestBehaviorDropEl) newRequestBehaviorDropEl.checked = true;
+ const currentSettings = await response.json();
+
+ // Populate Settings using the new response structure
+ if(portInputEl) portInputEl.value = currentSettings.serverPort || '';
+ if(requestTimeoutInputEl) requestTimeoutInputEl.value = currentSettings.requestTimeout !== null ? currentSettings.requestTimeout : '';
+ // Ping interval is not part of /admin/settings, assuming it's static or handled elsewhere if needed
+ // if(settingPingIntervalEl) settingPingIntervalEl.textContent = currentSettings.pingIntervalMs !== null ? `${currentSettings.pingIntervalMs} ms` : 'N/A (Not Implemented)';
+
+ if (currentSettings.messageSendStrategy === 'drop') {
+ 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';
- } 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 (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';
+
+ // Populate Status - Assuming server-info might still be used for uptime/connections or these are separate
+ // For now, focusing on settings. If status also needs to come from /admin/settings, that's another change.
+ // Let's assume uptime and connectedExtensionsCount are fetched separately or are less critical for this immediate fix.
+ // For simplicity, I'll comment out the status population from this specific fetch if it was tied to the old server-info structure.
+ // If your /admin/settings also returns uptime and connectedExtensionsCount, uncomment and adjust field names.
+ /*
+ if(statusUptimeEl) statusUptimeEl.textContent = formatUptime(currentSettings.uptimeSeconds);
+ if(statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = currentSettings.connectedExtensionsCount !== null ? currentSettings.connectedExtensionsCount : 'N/A';
+ */
+ // If you still have a /v1/admin/server-info for uptime and connections, that part can remain as is.
+ // This function will now primarily handle settings population.
+ // To keep status functional if it's from a different endpoint, we might need to separate concerns
+ // or ensure /admin/settings returns everything. For now, focusing on fixing settings load.
+
+ // Fetch separate status info if needed (example if /v1/admin/server-info is still used for status)
+ const statusResponse = await fetch('/v1/admin/server-info');
+ if (statusResponse.ok) {
+ const serverInfoForStatus = await statusResponse.json();
+ if(statusUptimeEl) statusUptimeEl.textContent = formatUptime(serverInfoForStatus.uptimeSeconds);
+ if(statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = serverInfoForStatus.connectedExtensionsCount !== null ? serverInfoForStatus.connectedExtensionsCount : 'N/A';
+ if(settingPingIntervalEl && serverInfoForStatus.pingIntervalMs !== undefined) settingPingIntervalEl.textContent = serverInfoForStatus.pingIntervalMs !== null ? `${serverInfoForStatus.pingIntervalMs} ms` : 'N/A (Not Implemented)';
+ } else {
+ // Handle case where statusResponse itself is not ok
+ console.warn(`Failed to fetch status from /v1/admin/server-info: ${statusResponse.status}`);
+ if(statusUptimeEl) statusUptimeEl.textContent = 'Status N/A';
+ if(statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = 'Status N/A';
+ if(settingPingIntervalEl) settingPingIntervalEl.textContent = 'Status N/A';
+ }
+ } catch (error) { // This is the single catch block for the try starting at line 310
+ console.error('Error in fetchAndDisplayServerInfo:', error);
+ // Set all relevant fields to an error state
+ if(portInputEl) portInputEl.value = 'Error';
+ if(requestTimeoutInputEl) requestTimeoutInputEl.value = 'Error';
+ if(settingPingIntervalEl) settingPingIntervalEl.textContent = 'Error';
+ if(newRequestBehaviorQueueEl) newRequestBehaviorQueueEl.checked = true; // Default on error
+ if(statusUptimeEl) statusUptimeEl.textContent = 'Error loading';
+ if(statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = 'Error loading';
}
}
+
async function handleSaveSettings() {
- if (!requestTimeoutInputEl || !portInputEl || !updateStatusMsgEl || !newRequestBehaviorQueueEl || !newRequestBehaviorDropEl || !autoKillPortInputEl) return;
+ if (!requestTimeoutInputEl || !portInputEl || !updateStatusMsgEl || !newRequestBehaviorQueueEl || !newRequestBehaviorDropEl) 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;
+ // UPDATED: Key name for server
+ settingsToUpdate.requestTimeout = newTimeout;
} else {
messages.push('Invalid timeout: Must be a positive number.');
validationError = true;
}
}
+
if (portInputEl.value.trim() !== '') { // Only process if there's input
- if (!isNaN(newPort) && newPort > 0 && newPort <= 65535) {
- settingsToUpdate.port = newPort;
+ if (!isNaN(newPort) && newPort > 0 && newPort <= 65535) {
+ // UPDATED: Key name for server
+ settingsToUpdate.serverPort = 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
+ // UPDATED: Key name for server
+ settingsToUpdate.messageSendStrategy = selectedNewRequestBehavior;
+
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', {
+ // UPDATED: Endpoint for saving settings
+ const response = await fetch('/admin/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settingsToUpdate)
@@ -471,9 +646,11 @@
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 {
@@ -486,13 +663,61 @@
}
}
}
+
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.");
+
+ // Initial load for settings is handled by tab click or initial messages tab load.
+ // fetchAndDisplayServerInfo(); // This is called when messages tab is active or settings tab is clicked.
+
+ // Collapsible column logic
+ const columnStates = { // To store the collapsed state
+ fromClient: false,
+ toExtension: false,
+ fromExtension: false,
+ toClient: false
+ };
+
+ function toggleColumn(columnKey) {
+ columnStates[columnKey] = !columnStates[columnKey];
+ const isCollapsed = columnStates[columnKey];
+
+ // Update button text/indicator
+ const button = document.querySelector(`.collapse-btn[data-column="${columnKey}"]`);
+ if (button) {
+ button.textContent = isCollapsed ? '+' : '-';
+ button.title = isCollapsed ? 'Expand Column' : 'Collapse Column';
+ }
+
+ // Toggle header visibility (optional, if you want to hide header text too)
+ // const headerCell = document.querySelector(`th.col-data[data-column-key="${columnKey}"]`); // Need to add data-column-key to th if this is desired
+
+ // Toggle data cell visibility (these are the
elements)
+ const cellsToToggle = document.querySelectorAll(`#message-history-body td.cell-${columnKey}`);
+ cellsToToggle.forEach(cell => {
+ cell.style.display = isCollapsed ? 'none' : '';
+ });
+
+ // Special handling for the header cell ( | ) due to table-layout: fixed
+ const headerTh = document.querySelector(`th[data-column="${columnKey}"]`);
+ if (headerTh) {
+ if (isCollapsed) {
+ headerTh.classList.add('header-collapsed');
+ } else {
+ headerTh.classList.remove('header-collapsed');
+ }
+ }
+ }
+
+ document.querySelectorAll('.collapse-btn').forEach(button => {
+ button.addEventListener('click', function() {
+ const columnKey = this.dataset.column;
+ toggleColumn(columnKey);
+ });
+ });
+
+ console.log("Admin UI initialized. Message history, settings, status, restart, and collapsible columns functionality implemented.");
-
\ No newline at end of file
diff --git a/api-relay-server/src/index.js b/api-relay-server/src/index.js
deleted file mode 100644
index 3cad791..0000000
--- a/api-relay-server/src/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * 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/.
- */
-// Import the server
-require('./server');
diff --git a/api-relay-server/src/server.js b/api-relay-server/src/server.js
deleted file mode 100644
index fdcdaca..0000000
--- a/api-relay-server/src/server.js
+++ /dev/null
@@ -1,473 +0,0 @@
-/*
- * 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/.
- */
-const express = require('express');
-const bodyParser = require('body-parser');
-const cors = require('cors');
-const { WebSocketServer } = require('ws');
-const http = require('http');
-
-// Create Express app
-const app = express();
-app.use(cors());
-app.use(bodyParser.json({ limit: '50mb' }));
-
-// Health check endpoint
-app.get('/health', (req, res) => {
- const aliveConnections = activeConnections.filter(conn => conn.isAlive);
- res.status(200).json({
- status: 'ok',
- timestamp: new Date().toISOString(),
- activeBrowserConnections: aliveConnections.length,
- totalTrackedBrowserConnections: activeConnections.length,
- webSocketServerState: wss.options.server.listening ? 'listening' : 'not_listening' // wss.readyState is not standard for server
- });
-});
-
-// Create HTTP server
-const server = http.createServer(app);
-
-// Create WebSocket server for browser extension communication
-const wss = new WebSocketServer({ server });
-
-// Global variables
-let activeConnections = [];
-const pendingRequests = new Map();
-let requestCounter = 0;
-
-// Connection health check interval (in milliseconds)
-const PING_INTERVAL = 30000; // 30 seconds
-const CONNECTION_TIMEOUT = 45000; // 45 seconds
-
-// Handle WebSocket connections from browser extensions
-wss.on('connection', (ws, req) => { // Added req to log client IP
- const clientIp = req.socket.remoteAddress;
- console.log(`SERVER: Browser extension connected from IP: ${clientIp}`);
-
- // Initialize connection state
- ws.isAlive = true;
- ws.pendingPing = false;
- ws.lastActivity = Date.now();
-
- // Add to active connections
- activeConnections.push(ws);
-
- // Set up ping interval for this connection
- const pingInterval = setInterval(() => {
- // Check if connection is still alive
- if (!ws.isAlive) {
- console.log('Browser extension connection timed out, terminating');
- clearInterval(pingInterval);
- ws.terminate();
- return;
- }
-
- // If we're still waiting for a pong from the last ping, mark as not alive
- if (ws.pendingPing) {
- console.log('Browser extension not responding to ping, marking as inactive');
- ws.isAlive = false;
- return;
- }
-
- // Check if there's been activity recently
- const inactiveTime = Date.now() - ws.lastActivity;
- if (inactiveTime > CONNECTION_TIMEOUT) {
- console.log(`Browser extension inactive for ${inactiveTime}ms, sending ping`);
- // Send a ping to check if still alive
- ws.pendingPing = true;
- try {
- ws.ping();
- } catch (error) {
- console.error('Error sending ping:', error);
- ws.isAlive = false;
- }
- }
- }, PING_INTERVAL);
-
- // Handle pong messages (response to ping)
- ws.on('pong', () => {
- ws.isAlive = true;
- ws.pendingPing = false;
- ws.lastActivity = Date.now();
- console.log('Browser extension responded to ping');
- });
-
- // Handle messages from browser extension
- ws.on('message', (messageBuffer) => {
- const rawMessage = messageBuffer.toString();
- console.log(`SERVER: Received raw message from extension (IP: ${clientIp}): ${rawMessage.substring(0, 500)}${rawMessage.length > 500 ? '...' : ''}`);
- try {
- // Update last activity timestamp
- ws.lastActivity = Date.now();
-
- const data = JSON.parse(rawMessage);
- console.log(`SERVER: Parsed message data from extension (IP: ${clientIp}):`, data);
-
- const { requestId, type } = data;
-
- if (requestId === undefined) {
- console.warn(`SERVER: Received message without requestId from IP ${clientIp}:`, data);
- // Handle other non-request-specific messages if any (e.g., status pings initiated by extension)
- if (type === 'EXTENSION_STATUS') {
- console.log(`SERVER: Browser extension status from IP ${clientIp}: ${data.status}`);
- }
- return;
- }
-
- // Log based on new message types from background.js
- if (type === 'CHAT_RESPONSE_CHUNK') {
- const chunkContent = data.chunk ? data.chunk.substring(0, 200) + (data.chunk.length > 200 ? '...' : '') : '[empty chunk]';
- console.log(`SERVER: Received CHAT_RESPONSE_CHUNK for requestId: ${requestId} from IP ${clientIp}. Chunk (first 200): ${chunkContent}. IsFinal: ${data.isFinal}`);
-
- const pendingRequest = pendingRequests.get(requestId);
- if (pendingRequest) {
- console.log(`SERVER: Processing CHAT_RESPONSE_CHUNK for pending request ${requestId} from IP ${clientIp}. IsFinal: ${data.isFinal}, Chunk (first 200): ${chunkContent}`);
- // Initialize accumulatedChunks if it doesn't exist (should be set on creation)
- if (typeof pendingRequest.accumulatedChunks === 'undefined') {
- pendingRequest.accumulatedChunks = '';
- }
-
- if (data.chunk) { // Ensure chunk is not null or undefined
- pendingRequest.accumulatedChunks += data.chunk;
- }
-
- if (data.isFinal) {
- console.log(`SERVER: Request ${requestId} (IP: ${clientIp}) received final CHAT_RESPONSE_CHUNK. Attempting to resolve promise.`);
- if (pendingRequest.timeoutId) {
- clearTimeout(pendingRequest.timeoutId);
- console.log(`SERVER: Request ${requestId} (IP: ${clientIp}) timeout cleared.`);
- }
- pendingRequest.resolve(pendingRequest.accumulatedChunks);
- pendingRequests.delete(requestId);
- console.log(`SERVER: Request ${requestId} (IP: ${clientIp}) promise resolved and removed from pending. Total length: ${pendingRequest.accumulatedChunks.length}`);
- } else {
- console.log(`SERVER: Accumulated chunk for requestId ${requestId} (IP: ${clientIp}). Current total length: ${pendingRequest.accumulatedChunks.length}`);
- }
- } else {
- console.log(`SERVER: Received CHAT_RESPONSE_CHUNK for request ${requestId} (IP: ${clientIp}, isFinal: ${data.isFinal}), but no pending request found.`);
- }
- } else if (type === 'CHAT_RESPONSE_STREAM_ENDED') {
- const pendingRequestStream = pendingRequests.get(requestId);
- if (pendingRequestStream) {
- console.log(`SERVER: Processing CHAT_RESPONSE_STREAM_ENDED for pending request ${requestId} (IP: ${clientIp}).`);
- // This message type now primarily signals the end. The actual data comes in CHAT_RESPONSE_CHUNK.
- // If a request is still pending and we haven't resolved it with a final chunk,
- // it might indicate an issue or a stream that ended without complete data.
- if (!pendingRequestStream.resolved) {
- console.warn(`SERVER: Stream ended for requestId ${requestId} (IP: ${clientIp}), but request was not fully resolved with data. This might be an issue.`);
- }
- } else {
- console.log(`SERVER: Received CHAT_RESPONSE_STREAM_ENDED for request ${requestId} (IP: ${clientIp}), but no pending request found.`);
- }
- } else if (type === 'CHAT_RESPONSE_ERROR') {
- const errorMsg = data.error || "Unknown error from extension.";
- console.error(`SERVER: Received CHAT_RESPONSE_ERROR for requestId: ${requestId} (IP: ${clientIp}). Error: ${errorMsg}`);
- const pendingRequestError = pendingRequests.get(requestId);
- if (pendingRequestError) {
- console.log(`SERVER: Processing CHAT_RESPONSE_ERROR for pending request ${requestId} (IP: ${clientIp}).`);
- if (pendingRequestError.timeoutId) {
- clearTimeout(pendingRequestError.timeoutId);
- console.log(`SERVER: Request ${requestId} (IP: ${clientIp}) timeout cleared due to error.`);
- }
- pendingRequestError.reject(new Error(`Extension reported error for request ${requestId}: ${errorMsg}`));
- pendingRequests.delete(requestId);
- console.log(`SERVER: Request ${requestId} (IP: ${clientIp}) rejected due to CHAT_RESPONSE_ERROR and removed from pending.`);
- } else {
- console.log(`SERVER: Received CHAT_RESPONSE_ERROR for request ${requestId} (IP: ${clientIp}), but no pending request found.`);
- }
- } else if (type === 'CHAT_RESPONSE') { // Keep old CHAT_RESPONSE for compatibility if content script DOM fallback sends it
- const { response } = data;
- console.log(`SERVER: Received (legacy) CHAT_RESPONSE for requestId: ${requestId} from IP ${clientIp}. Response (first 100): ${response ? response.substring(0,100) : '[empty]'}`);
- const pendingRequest = pendingRequests.get(requestId);
- if (pendingRequest) {
- if (pendingRequest.timeoutId) clearTimeout(pendingRequest.timeoutId);
- pendingRequest.resolve(response);
- pendingRequests.delete(requestId);
- console.log(`SERVER: Request ${requestId} resolved with (legacy) CHAT_RESPONSE from IP ${clientIp}.`);
- } else {
- console.log(`SERVER: Received (legacy) CHAT_RESPONSE for request ${requestId} from IP ${clientIp}, but no pending request found.`);
- }
- } else if (type === 'EXTENSION_ERROR') { // General extension error not tied to a request
- console.error(`SERVER: Browser extension (IP: ${clientIp}) reported general error: ${data.error}`);
- } else if (type === 'EXTENSION_STATUS') {
- console.log(`SERVER: Browser extension (IP: ${clientIp}) status: ${data.status}`);
- } else {
- console.warn(`SERVER: Received unknown message type '${type}' from IP ${clientIp} for requestId ${requestId}:`, data);
- }
- } catch (error) {
- console.error(`SERVER: Error processing WebSocket message from IP ${clientIp}:`, error, `Raw message: ${rawMessage}`);
- }
- });
-
- // Handle disconnection
- ws.on('close', (code, reason) => {
- const reasonString = reason ? reason.toString() : 'No reason given';
- console.log(`SERVER: Browser extension (IP: ${clientIp}) disconnected. Code: ${code}, Reason: ${reasonString}`);
- clearInterval(pingInterval);
- activeConnections = activeConnections.filter(conn => conn !== ws);
-
- // Check if there are any pending requests that were using this connection
- // and reject them with a connection closed error
- pendingRequests.forEach((request, requestId) => {
- if (request.connection === ws) {
- console.log(`Rejecting request ${requestId} due to connection close`);
- request.reject(new Error('Browser extension disconnected'));
- pendingRequests.delete(requestId);
- }
- });
- });
-
- // Handle errors
- ws.on('error', (error) => {
- console.error(`SERVER: WebSocket error for connection from IP ${clientIp}:`, error);
- ws.isAlive = false; // Mark as not alive on error
- // Consider terminating and cleaning up like in 'close' if error is fatal
- });
-});
-
-// Create API router
-const apiRouter = express.Router();
-
-// Configuration
-const REQUEST_TIMEOUT = 300000; // 5 minutes (in milliseconds)
-const MAX_RETRIES = 2; // Maximum number of retries for a failed request
-
-// Helper function to find the best active connection
-function getBestConnection() {
- // Filter out connections that are not alive
- const aliveConnections = activeConnections.filter(conn => conn.isAlive);
-
- if (aliveConnections.length === 0) {
- return null;
- }
-
- // Sort connections by last activity (most recent first)
- aliveConnections.sort((a, b) => b.lastActivity - a.lastActivity);
-
- return aliveConnections[0];
-}
-
-// OpenAI-compatible chat completions endpoint
-apiRouter.post('/chat/completions', async (req, res) => {
- try {
- const { messages, model, temperature, max_tokens } = req.body;
- console.log(`SERVER: Full incoming HTTP request body for request ID (to be generated):`, JSON.stringify(req.body, null, 2));
-
- // Generate a unique request ID
- const requestId = requestCounter++;
-
- // Extract the user's message (last message in the array)
- const userMessage = messages[messages.length - 1].content;
-
- // Get the best active connection
- const extension = getBestConnection();
-
- // Check if we have any active connections
- if (!extension) {
- return res.status(503).json({
- error: {
- message: "No active browser extension connected. Please open the chat interface and ensure the extension is active.",
- type: "server_error",
- code: "no_extension_connected"
- }
- });
- }
-
- // Create a promise that will be resolved when the response is received
- console.log(`SERVER: Request ${requestId} creating response promise.`);
- const responsePromise = new Promise((resolve, reject) => {
- const internalResolve = (value) => {
- console.log(`SERVER: Request ${requestId} internal promise resolve function called.`);
- resolve(value);
- };
- const internalReject = (reason) => {
- console.log(`SERVER: Request ${requestId} internal promise reject function called.`);
- reject(reason);
- };
-
- // Set a timeout to reject the promise after the configured timeout
- const timeoutId = setTimeout(() => {
- if (pendingRequests.has(requestId)) {
- console.error(`SERVER: Request ${requestId} timed out after ${REQUEST_TIMEOUT}ms. Rejecting promise.`);
- pendingRequests.delete(requestId); // Ensure cleanup
- internalReject(new Error('Request timed out'));
- } else {
- console.warn(`SERVER: Request ${requestId} timeout triggered, but request no longer in pendingRequests. It might have resolved or errored just before timeout.`);
- }
- }, REQUEST_TIMEOUT);
-
- // Store the promise resolvers and the connection being used
- pendingRequests.set(requestId, {
- resolve: internalResolve,
- reject: internalReject,
- connection: extension,
- timeoutId,
- retryCount: 0,
- accumulatedChunks: '' // Initialize for chunk accumulation
- });
- console.log(`SERVER: Request ${requestId} added to pendingRequests. Timeout ID: ${timeoutId}`);
- });
-
- // Prepare the message
- const message = {
- type: 'SEND_CHAT_MESSAGE',
- requestId,
- message: userMessage,
- settings: {
- model,
- temperature,
- max_tokens
- }
- };
-
- // Send the message to the browser extension
- try {
- console.log(`SERVER: Request ${requestId} - Sending full message to extension:`, JSON.stringify(message, null, 2));
- extension.send(JSON.stringify(message));
- console.log(`SERVER: Request ${requestId} (message type: ${message.type}) sent to browser extension (IP: ${extension.remoteAddress || 'unknown'}). Waiting for response...`);
-
- // Update last activity timestamp
- extension.lastActivity = Date.now();
- } catch (error) {
- console.error(`Error sending message to extension for request ${requestId}:`, error);
-
- // Clean up the pending request
- if (pendingRequests.has(requestId)) {
- const pendingRequest = pendingRequests.get(requestId);
- if (pendingRequest.timeoutId) {
- clearTimeout(pendingRequest.timeoutId);
- }
- pendingRequests.delete(requestId);
- }
-
- return res.status(500).json({
- error: {
- message: "Failed to send message to browser extension",
- type: "server_error",
- code: "extension_communication_error"
- }
- });
- }
-
- // Wait for the response
- const awaitStartTime = Date.now();
- console.log(`SERVER: Request ${requestId} is now awaiting responsePromise (extension response). Timeout set to ${REQUEST_TIMEOUT}ms.`);
- const response = await responsePromise;
- const awaitEndTime = Date.now();
- console.log(`SERVER: Request ${requestId} await responsePromise completed in ${awaitEndTime - awaitStartTime}ms. Received response from extension. Preparing to send to client.`);
-
- // Format the response in OpenAI format
- const formatStartTime = Date.now();
- const formattedResponse = {
- id: `chatcmpl-${Date.now()}`,
- object: "chat.completion",
- created: Math.floor(Date.now() / 1000),
- model: model || "relay-model", // model is from req.body
- choices: [
- {
- index: 0,
- message: {
- role: "assistant",
- content: response // response is the string from the extension
- },
- finish_reason: "stop"
- }
- ],
- usage: {
- prompt_tokens: -1, // We don't track tokens
- completion_tokens: -1,
- total_tokens: -1
- }
- // Removed service_tier, logprobs, refusal, annotations, and detailed usage to match simpler working version
- };
-
- console.log(`SERVER: Request ${requestId} - Full outgoing HTTP response body:`, JSON.stringify(formattedResponse, null, 2));
- res.json(formattedResponse);
- const sendEndTime = Date.now();
- console.log(`SERVER: Request ${requestId} formatted and sent response to client in ${sendEndTime - formatStartTime}ms (total after await: ${sendEndTime - awaitEndTime}ms).`);
- } catch (error) {
- const reqIdForLog = typeof requestId !== 'undefined' ? requestId : (error && typeof error.requestId !== 'undefined' ? error.requestId : 'UNKNOWN');
- console.error(`SERVER: Error processing chat completion for request ${reqIdForLog}:`, error);
- if (typeof requestId === 'undefined') {
- console.error(`SERVER: CRITICAL - 'requestId' was undefined in catch block. Error object requestId: ${error && error.requestId}`);
- }
-
- // Determine the appropriate status code based on the error
- let statusCode = 500;
- let errorType = "server_error";
- let errorCode = "internal_error";
-
- if (error.message === 'Request timed out') {
- statusCode = 504; // Gateway Timeout
- errorType = "timeout_error";
- errorCode = "request_timeout";
- } else if (error.message === 'Browser extension disconnected') {
- statusCode = 503; // Service Unavailable
- errorType = "server_error";
- errorCode = "extension_disconnected";
- }
-
- const errorResponsePayload = {
- error: {
- message: error.message,
- type: errorType,
- code: errorCode
- }
- };
- console.log(`SERVER: Request ${reqIdForLog} - Sending error response to client:`, JSON.stringify(errorResponsePayload, null, 2));
- res.status(statusCode).json(errorResponsePayload);
- }
-});
-
-// Models endpoint
-apiRouter.get('/models', (req, res) => {
- res.json({
- object: "list",
- data: [
- {
- id: "gemini-pro",
- object: "model",
- created: 1677610602,
- owned_by: "relay"
- },
- {
- id: "chatgpt",
- object: "model",
- created: 1677610602,
- owned_by: "relay"
- },
- {
- id: "claude-3",
- object: "model",
- created: 1677610602,
- owned_by: "relay"
- }
- ]
- });
-});
-
-// Mount the API router
-app.use('/v1', apiRouter);
-
-// Start the server
-const PORT = process.env.PORT || 3003;
-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}`);
-});
-
-module.exports = server;
diff --git a/api-relay-server/src/server.ts b/api-relay-server/src/server.ts
index 5cebc2c..f5ef824 100644
--- a/api-relay-server/src/server.ts
+++ b/api-relay-server/src/server.ts
@@ -1,3 +1,5 @@
+#!/usr/bin/env node
+console.log("SERVER.TS: Top of file reached - src/server.ts is being executed.");
/*
* Chat Relay: Relay for AI Chat Interfaces
* Copyright (C) 2025 Jamison Moore
@@ -15,14 +17,47 @@
* 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 express, { NextFunction, Request, Response, Router } from 'express';
-import fs from 'fs';
+import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';
import path from 'path';
-import { WebSocket, WebSocketServer } from 'ws';
+import fs from 'fs';
+import os from 'os';
+
+// Load .env file variables into process.env
+const envPath = path.resolve(__dirname, '../relay.settings');
+
+if (fs.existsSync(envPath)) {
+ const lines = fs.readFileSync(envPath, 'utf-8').split('\n');
+ for (const line of lines) {
+ const [key, ...vals] = line.split('=');
+ if (key && vals.length) process.env[key.trim()] = vals.join('=').trim();
+ }
+ console.log(`SERVER.TS: relay.settings loaded from ${envPath}`);
+} else {
+ console.warn(`SERVER.TS: relay.settings file not found at ${envPath}`);
+}
+
+const ENV_FILE_PATH = envPath;
+const TS_VALID_STRATEGIES = ["queue", "drop"];
+
+// --- Initialize settings from .env or use defaults ---
+let newRequestBehavior: 'queue' | 'drop' = process.env.MESSAGE_SEND_STRATEGY && TS_VALID_STRATEGIES.includes(process.env.MESSAGE_SEND_STRATEGY as any)
+ ? process.env.MESSAGE_SEND_STRATEGY as 'queue' | 'drop'
+ : 'queue';
+
+let currentRequestTimeoutMs: number = process.env.REQUEST_TIMEOUT_MS && !isNaN(parseInt(process.env.REQUEST_TIMEOUT_MS, 10)) && parseInt(process.env.REQUEST_TIMEOUT_MS, 10) > 0
+ ? parseInt(process.env.REQUEST_TIMEOUT_MS, 10)
+ : 120000;
+
+let serverPortForListen: number = process.env.PORT && !isNaN(parseInt(process.env.PORT, 10)) && parseInt(process.env.PORT, 10) > 0
+ ? parseInt(process.env.PORT, 10)
+ : 3003;
+
+console.log(`SERVER.TS: Initial effective settings - Strategy: ${newRequestBehavior}, Timeout: ${currentRequestTimeoutMs}ms, Port: ${serverPortForListen}`);
+
// Interfaces
interface PendingRequest {
resolve: (value: any) => void;
@@ -31,11 +66,11 @@ interface PendingRequest {
interface WebSocketMessage {
type: string;
requestId?: number;
- message?: string; // For messages sent from server to extension
- response?: string; // For CHAT_RESPONSE from extension (older DOM method)
- chunk?: string; // For CHAT_RESPONSE_CHUNK from extension (debugger method)
- isFinal?: boolean; // Flag for CHAT_RESPONSE_CHUNK
- error?: string; // For CHAT_RESPONSE_ERROR from extension
+ message?: string;
+ response?: string;
+ chunk?: string;
+ isFinal?: boolean;
+ error?: string;
settings?: {
model?: string;
temperature?: number;
@@ -43,14 +78,10 @@ interface WebSocketMessage {
};
}
-// Queuing/Dropping System State
-let activeExtensionProcessingId: number | null = null;
-// newRequestBehavior will be initialized after loadServerConfig()
-let newRequestBehavior: 'queue' | 'drop';
interface QueuedRequest {
requestId: number;
- req: Request; // Express Request object
- res: Response; // Express Response object
+ req: Request;
+ res: Response;
userMessage: string;
model?: string;
temperature?: number;
@@ -62,6 +93,8 @@ let requestQueue: QueuedRequest[] = [];
let activeConnections: WebSocket[] = [];
const pendingRequests = new Map();
let requestCounter = 0;
+let activeExtensionProcessingId: number | null = null;
+let activeExtensionSocketId: string | null = null; // Stores the socketId of the extension processing the current request
// In-memory store for admin messages
interface ModelSettings {
@@ -69,143 +102,245 @@ interface ModelSettings {
temperature?: number;
max_tokens?: number;
}
-
interface ChatRequestData {
fromClient: string;
- toExtension: WebSocketMessage; // Assuming WebSocketMessage is defined elsewhere
+ toExtension: WebSocketMessage;
modelSettings: ModelSettings;
}
-
interface ChatResponseData {
fromExtension: string;
- toClient: any; // This could be more specific if the OpenAI response structure is defined
+ toClient: any;
status: string;
}
-
interface ChatErrorData {
- toClientError: any; // This could be more specific if the error JSON structure is defined
+ toClientError: any;
status: string;
}
-
-type AdminLogDataType = ChatRequestData | ChatResponseData | ChatErrorData | any; // Fallback to any for other types
-
+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'
+ | 'REQUEST_CANCELLED_DISCONNECT'
+ | 'REQUEST_CANCELLED_TIMEOUT'
+ | 'REQUEST_CANCELLED_REFRESH'
+ | 'EXTENSION_READY'
+ | 'SETTING_UPDATE'
+ | string;
requestId: string;
data: AdminLogDataType;
}
const MAX_ADMIN_HISTORY_LENGTH = 1000;
let adminMessageHistory: AdminLogEntry[] = [];
-const serverStartTime = Date.now(); // Store server start time
+const serverStartTime = Date.now();
-// Configuration file path
-const CONFIG_FILE_PATH = path.join(__dirname, 'server-config.json');
-const RESTART_TRIGGER_FILE_PATH = path.join(__dirname, '.triggerrestart'); // For explicitly triggering nodemon
-
-interface ServerConfig {
- port?: number;
- requestTimeoutMs?: number;
- lastRestartRequestTimestamp?: number; // New field
- newRequestBehavior?: 'queue' | 'drop';
- autoKillPort?: boolean; // New setting for auto-killing port
-}
-
-// Function to read configuration
-function loadServerConfig(): ServerConfig {
- try {
- if (fs.existsSync(CONFIG_FILE_PATH)) {
- const configFile = fs.readFileSync(CONFIG_FILE_PATH, 'utf-8');
- return JSON.parse(configFile) as ServerConfig;
+// Function to update .env file
+function updateEnvFile(settingsToUpdate: Record) {
+ let envContent = "";
+ if (fs.existsSync(ENV_FILE_PATH)) {
+ envContent = fs.readFileSync(ENV_FILE_PATH, 'utf8');
+ }
+ let lines = envContent.split(os.EOL).map(line => line.trim()).filter(line => line);
+ const newLinesToAdd: string[] = [];
+ for (const key in settingsToUpdate) {
+ const value = settingsToUpdate[key];
+ const settingLine = `${key}=${value}`;
+ let found = false;
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].startsWith(`${key}=`)) {
+ lines[i] = settingLine;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ newLinesToAdd.push(settingLine);
}
- } catch (error) {
- console.error('Error reading server-config.json, using defaults/env vars:', error);
}
- return {};
-}
-
-// Function to write configuration
-function saveServerConfig(config: ServerConfig): void {
+ lines = lines.concat(newLinesToAdd);
+ const finalContent = lines.join(os.EOL) + (lines.length > 0 ? os.EOL : '');
try {
- fs.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(config, null, 2), 'utf-8');
- console.log('Server configuration saved to server-config.json');
+ fs.writeFileSync(ENV_FILE_PATH, finalContent, 'utf8');
+ console.log(`SERVER.TS: Settings saved to ${ENV_FILE_PATH}. Content: \n${finalContent}`);
} catch (error) {
- console.error('Error writing server-config.json:', error);
+ console.error(`SERVER.TS: Error saving settings to ${ENV_FILE_PATH}:`, error);
}
}
-// Load initial config
-const initialConfig = loadServerConfig();
+// Ensure .env file exists with current/default settings
+const initialEnvSettingsToEnsure: Record = {};
+if (!process.env.MESSAGE_SEND_STRATEGY || !TS_VALID_STRATEGIES.includes(process.env.MESSAGE_SEND_STRATEGY as any)) {
+ initialEnvSettingsToEnsure.MESSAGE_SEND_STRATEGY = newRequestBehavior;
+}
+if (!process.env.REQUEST_TIMEOUT_MS || isNaN(parseInt(process.env.REQUEST_TIMEOUT_MS, 10)) || parseInt(process.env.REQUEST_TIMEOUT_MS, 10) <= 0) {
+ initialEnvSettingsToEnsure.REQUEST_TIMEOUT_MS = currentRequestTimeoutMs;
+}
+if (!process.env.PORT || isNaN(parseInt(process.env.PORT, 10)) || parseInt(process.env.PORT, 10) <= 0) {
+ initialEnvSettingsToEnsure.PORT = serverPortForListen;
+}
+if (Object.keys(initialEnvSettingsToEnsure).length > 0) {
+ console.log("SERVER.TS: Initializing/updating .env file with current/default settings:", initialEnvSettingsToEnsure);
+ updateEnvFile(initialEnvSettingsToEnsure);
+} else {
+ console.log(`SERVER.TS: .env file at ${ENV_FILE_PATH} found and all required settings appear present.`);
+}
-// Initialize newRequestBehavior from config, defaulting to 'queue'
-newRequestBehavior = initialConfig.newRequestBehavior && (initialConfig.newRequestBehavior === 'queue' || initialConfig.newRequestBehavior === 'drop')
- ? 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);
// Create Express app
const app = express();
app.use(cors());
-app.use(bodyParser.json({ limit: '10mb' })); // Increased payload size limit
-// Admin UI: Serve static files
-// Correct path considering TypeScript's outDir. __dirname will be 'dist' at runtime.
-const adminUIDirectory = path.join(__dirname, '../src/admin-ui');
-app.use('/admin-static', express.static(adminUIDirectory));
-// Admin UI: Route for the main admin page
-app.get('/admin', (req: Request, res: Response) => {
- res.sendFile(path.join(adminUIDirectory, 'admin.html'));
+app.use(bodyParser.json({ limit: '10mb' }));
+
+// Middleware to log all request paths
+app.use((req: Request, res: Response, next: NextFunction) => {
+ console.log(`SERVER.TS: Incoming request: ${req.method} ${req.path}`);
+ next();
});
-// Create HTTP server
+
+console.log(`SERVER.TS: Loaded - Strategy: ${newRequestBehavior}, Timeout: ${currentRequestTimeoutMs}ms, Port: ${serverPortForListen}`);
+
+// Interface for Admin Settings Payload
+interface AdminSettingsPayload {
+ messageSendStrategy?: 'queue' | 'drop';
+ requestTimeout?: number | string; // Allow string from form, parse to number
+ serverPort?: number | string; // Allow string from form, parse to number
+}
+
+// --- Admin API Routes (using .env) ---
+app.get('/admin/settings', (req: Request, res: Response) => {
+ console.log(`SERVER.TS: Handling GET /admin/settings. Current settings: Strategy=${newRequestBehavior}, Timeout=${currentRequestTimeoutMs}, Port=${serverPortForListen}`);
+ res.status(200).json({
+ messageSendStrategy: newRequestBehavior,
+ requestTimeout: currentRequestTimeoutMs,
+ serverPort: serverPortForListen
+ });
+});
+console.log("SERVER.TS: DEFINED ROUTE: GET /admin/settings");
+
+app.post('/admin/settings', (req: Request<{}, any, AdminSettingsPayload>, res: Response) => {
+ const { messageSendStrategy, requestTimeout, serverPort } = req.body;
+ let changesMade = false;
+ let errors: string[] = [];
+ const settingsToSaveToEnv: Record = {};
+
+ if (messageSendStrategy !== undefined) {
+ if (TS_VALID_STRATEGIES.includes(messageSendStrategy as any)) {
+ newRequestBehavior = messageSendStrategy as 'queue' | 'drop';
+ settingsToSaveToEnv.MESSAGE_SEND_STRATEGY = newRequestBehavior;
+ console.log(`SERVER.TS: Handling POST /admin/settings - Strategy updated to: ${newRequestBehavior}`);
+ changesMade = true;
+ } else {
+ errors.push(`Invalid messageSendStrategy. Must be one of: ${TS_VALID_STRATEGIES.join(', ')}`);
+ console.warn(`SERVER.TS: Handling POST /admin/settings - Invalid strategy: ${messageSendStrategy}`);
+ }
+ }
+
+ if (requestTimeout !== undefined) {
+ const timeoutMs = parseInt(String(requestTimeout), 10);
+ if (!isNaN(timeoutMs) && timeoutMs > 0) {
+ currentRequestTimeoutMs = timeoutMs;
+ settingsToSaveToEnv.REQUEST_TIMEOUT_MS = currentRequestTimeoutMs;
+ console.log(`SERVER.TS: Handling POST /admin/settings - Timeout updated to: ${currentRequestTimeoutMs}ms`);
+ changesMade = true;
+ } else {
+ errors.push('Invalid requestTimeout. Must be a positive number.');
+ console.warn(`SERVER.TS: Handling POST /admin/settings - Invalid timeout: ${requestTimeout}`);
+ }
+ }
+
+ if (serverPort !== undefined) {
+ const portNum = parseInt(String(serverPort), 10);
+ if (!isNaN(portNum) && portNum > 0 && portNum < 65536) {
+ serverPortForListen = portNum;
+ settingsToSaveToEnv.PORT = serverPortForListen;
+ console.log(`SERVER.TS: Handling POST /admin/settings - Port updated to: ${serverPortForListen}`);
+ changesMade = true;
+ } else {
+ errors.push('Invalid serverPort. Must be a number between 1 and 65535.');
+ console.warn(`SERVER.TS: Handling POST /admin/settings - Invalid port: ${serverPort}`);
+ }
+ }
+
+ if (errors.length > 0) {
+ res.status(400).json({ error: errors.join('; ') });
+ return;
+ }
+
+ if (changesMade && Object.keys(settingsToSaveToEnv).length > 0) {
+ updateEnvFile(settingsToSaveToEnv);
+ res.status(200).json({
+ message: 'Settings updated. .env file modified, server may restart.',
+ currentSettings: {
+ messageSendStrategy: newRequestBehavior,
+ requestTimeout: currentRequestTimeoutMs,
+ serverPort: serverPortForListen
+ }
+ });
+ } else {
+ res.status(200).json({
+ message: 'No valid settings were changed.',
+ currentSettings: {
+ messageSendStrategy: newRequestBehavior,
+ requestTimeout: currentRequestTimeoutMs,
+ serverPort: serverPortForListen
+ }
+ });
+ }
+});
+console.log("SERVER.TS: DEFINED ROUTE: POST /admin/settings");
+// --- End Admin API Routes ---
+
+// Admin UI: Serve static files
+const adminUIDirectory = path.join(__dirname, '../src/admin-ui');
+app.use('/admin', express.static(adminUIDirectory));
+app.get('/admin', (req: Request, res: Response) => { // Redirect /admin to /admin/admin.html
+ res.redirect('/admin/admin.html');
+});
+
+// Create HTTP server & WebSocket server
const server = http.createServer(app);
-// Create WebSocket server for browser extension communication
const wss = new WebSocketServer({ server });
-// Handle WebSocket connections from browser extensions
+
+// WebSocket server logic
wss.on('connection', (ws: WebSocket) => {
- console.log('Browser extension connected');
+ (ws as any).socketId = Date.now().toString(36) + Math.random().toString(36).substring(2); // Assign unique ID
+ console.log(`SERVER.TS: Browser extension connected with socketId: ${(ws as any).socketId}`);
activeConnections.push(ws);
- // Handle messages from browser extension
ws.on('message', (message: string) => {
try {
const data: WebSocketMessage = JSON.parse(message.toString());
- let requestIdToProcess: number | undefined = undefined;
+ const currentSocketId = (ws as any).socketId;
+ let requestIdToProcess: number | undefined = data.requestId;
let responseDataToUse: string | undefined = undefined;
let isErrorMessage = false;
- console.log(`SERVER: WebSocket message received from extension: type=${data.type}, requestId=${data.requestId}`);
+ console.log(`SERVER.TS: WebSocket message received from socketId ${currentSocketId}: type=${data.type}, requestId=${data.requestId}`);
- if (data.type === 'CHAT_RESPONSE') {
- requestIdToProcess = data.requestId;
+ if (data.type === 'EXTENSION_READY') {
+ console.log(`SERVER.TS: Extension with socketId ${currentSocketId} reported EXTENSION_READY.`);
+ logAdminMessage('EXTENSION_READY', 'N/A', { socketId: currentSocketId }).catch(err => console.error("ADMIN_LOG_ERROR:", err));
+ // Future: If the extension had a persistent ID, we could check if it was previously
+ // processing a request and mark it as "Cancelled (Extension Refreshed)".
+ // For now, the disconnect/reconnect logic based on socketId handles most stale states.
+ return;
+ } else if (data.type === 'CHAT_RESPONSE') {
responseDataToUse = data.response;
- console.log(`SERVER: Processing CHAT_RESPONSE for requestId: ${data.requestId}`);
} else if (data.type === 'CHAT_RESPONSE_CHUNK' && data.isFinal === true) {
- requestIdToProcess = data.requestId;
responseDataToUse = data.chunk;
- console.log(`SERVER: Processing final CHAT_RESPONSE_CHUNK for requestId: ${data.requestId}`);
} else if (data.type === 'CHAT_RESPONSE_ERROR') {
- requestIdToProcess = data.requestId;
responseDataToUse = data.error || "Unknown error from extension";
isErrorMessage = true;
- console.log(`SERVER: Processing CHAT_RESPONSE_ERROR for requestId: ${data.requestId}`);
} else if (data.type === 'CHAT_RESPONSE_STREAM_ENDED') {
- // This message type currently doesn't carry the final data itself in background.js,
- // the CHAT_RESPONSE_CHUNK with isFinal=true does.
- // So, we just log it. The promise should be resolved by the final CHUNK.
- console.log(`SERVER: Received CHAT_RESPONSE_STREAM_ENDED for requestId: ${data.requestId}. No action taken as final data comes in CHUNK.`);
+ console.log(`SERVER.TS: Received CHAT_RESPONSE_STREAM_ENDED for requestId: ${data.requestId} from socketId ${currentSocketId}. No action as final data comes in CHUNK.`);
return;
} else {
- console.log(`SERVER: Received unhandled WebSocket message type: ${data.type} for requestId: ${data.requestId}`);
+ console.log(`SERVER.TS: Unhandled WebSocket message type: ${data.type} for requestId: ${data.requestId} from socketId ${currentSocketId}`);
return;
}
@@ -213,547 +348,270 @@ wss.on('connection', (ws: WebSocket) => {
const pendingRequest = pendingRequests.get(requestIdToProcess);
if (pendingRequest) {
if (isErrorMessage) {
- console.error(`SERVER: Rejecting request ${requestIdToProcess} with error: ${responseDataToUse}`);
+ console.error(`SERVER.TS: 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.TS: Resolving request ${requestIdToProcess} with data (first 100 chars): ${(responseDataToUse || "").substring(0,100)}`);
pendingRequest.resolve(responseDataToUse);
}
pendingRequests.delete(requestIdToProcess);
- console.log(`SERVER: Request ${requestIdToProcess} ${isErrorMessage ? 'rejected' : 'resolved'} and removed from pending.`);
+ if (activeExtensionProcessingId === requestIdToProcess) { // Check if this was the active request
+ activeExtensionProcessingId = null;
+ activeExtensionSocketId = null; // Clear the socket ID
+ console.log(`SERVER.TS: Request ${requestIdToProcess} ${isErrorMessage ? 'rejected' : 'resolved'} and removed. Extension and socket ID freed.`);
+ } else {
+ console.log(`SERVER.TS: Request ${requestIdToProcess} ${isErrorMessage ? 'rejected' : 'resolved'} and removed. It was not the primary active request (${activeExtensionProcessingId}).`);
+ }
+ processNextInQueue();
} else {
- console.warn(`SERVER: Received response for unknown or timed-out requestId: ${requestIdToProcess}. No pending request found.`);
+ console.warn(`SERVER.TS: Received response for unknown/timed-out requestId: ${requestIdToProcess} from socketId ${currentSocketId}.`);
}
} else {
- // This case should ideally not be reached if the above 'if/else if' for types is exhaustive for messages carrying a requestId.
- console.warn(`SERVER: Received WebSocket message but could not determine requestId to process. Type: ${data.type}, Full Data:`, data);
+ console.warn(`SERVER.TS: WebSocket message received from socketId ${currentSocketId} but no requestId. Type: ${data.type}`);
}
} catch (error) {
- console.error('SERVER: Error processing WebSocket message:', error, 'Raw message:', message.toString());
+ const currentSocketIdForError = (ws as any).socketId;
+ console.error(`SERVER.TS: Error processing WebSocket message from socketId ${currentSocketIdForError}:`, error, 'Raw message:', message.toString());
}
});
- // Handle disconnection
ws.on('close', () => {
- console.log('Browser extension disconnected');
- activeConnections = activeConnections.filter(conn => conn !== ws);
+ const closedSocketId = (ws as any).socketId;
+ console.log(`SERVER.TS: Browser extension with socketId: ${closedSocketId} disconnected`);
+ activeConnections = activeConnections.filter(conn => (conn as any).socketId !== closedSocketId);
+
+ if (closedSocketId && closedSocketId === activeExtensionSocketId) {
+ console.log(`SERVER.TS: Extension ${closedSocketId} was processing request ${activeExtensionProcessingId}. Attempting to cancel.`);
+ if (activeExtensionProcessingId !== null) {
+ const pendingRequest = pendingRequests.get(activeExtensionProcessingId);
+ if (pendingRequest) {
+ const errorMessage = `Request ${activeExtensionProcessingId} cancelled: Extension ${closedSocketId} disconnected.`;
+ console.log(`SERVER.TS: ${errorMessage}`);
+ pendingRequest.reject(new Error(errorMessage));
+ pendingRequests.delete(activeExtensionProcessingId);
+ }
+ logAdminMessage('REQUEST_CANCELLED_DISCONNECT', activeExtensionProcessingId, {
+ reason: 'Extension disconnected',
+ socketId: closedSocketId
+ }).catch(err => console.error("ADMIN_LOG_ERROR:", err));
+
+ activeExtensionProcessingId = null;
+ activeExtensionSocketId = null;
+ console.log(`SERVER.TS: Freed up extension. activeExtensionProcessingId is now null. Processing next in queue.`);
+ processNextInQueue();
+ } else {
+ console.log(`SERVER.TS: Extension ${closedSocketId} disconnected, but no activeExtensionProcessingId was set for it. No request to cancel.`);
+ }
+ } else {
+ console.log(`SERVER.TS: Extension ${closedSocketId} disconnected, but it was not the active processor (active is ${activeExtensionSocketId}). No request cancellation needed from this event.`);
+ }
+ });
+ ws.on('error', (error) => {
+ console.error('SERVER.TS: WebSocket error:', error);
});
});
-// Function to log admin messages to in-memory store
-async function logAdminMessage(
- type: AdminLogEntry['type'], // Use the more specific type from AdminLogEntry
- requestId: string | number,
- data: AdminLogDataType // Use the specific union type for data
-): Promise {
+
+async function logAdminMessage(type: AdminLogEntry['type'], requestId: string | number, data: AdminLogDataType): Promise {
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));
-
- const logEntry: AdminLogEntry = {
- timestamp,
- type,
- requestId: String(requestId),
- data,
- };
-
+ const logEntry: AdminLogEntry = { timestamp, type, requestId: String(requestId), data };
adminMessageHistory.unshift(logEntry);
-
if (adminMessageHistory.length > MAX_ADMIN_HISTORY_LENGTH) {
- adminMessageHistory = adminMessageHistory.slice(0, MAX_ADMIN_HISTORY_LENGTH);
+ adminMessageHistory.pop();
}
}
-// Define processRequest
-async function processRequest(queuedItem: QueuedRequest): Promise {
+async function processOrQueueRequest(queuedItem: QueuedRequest): Promise {
const { requestId, req, res, userMessage, model, temperature, max_tokens } = queuedItem;
- activeExtensionProcessingId = requestId;
- logAdminMessage('CHAT_REQUEST_PROCESSING', requestId, { status: 'Sending to extension', activeExtensionProcessingId })
- .catch(err => console.error("ADMIN_LOG_ERROR (CHAT_REQUEST_PROCESSING):", err));
- console.log(`SERVER: Processing request ${requestId}. ActiveExtensionProcessingId: ${activeExtensionProcessingId}`);
+ logAdminMessage('CHAT_REQUEST_RECEIVED', requestId, {
+ fromClient: userMessage,
+ modelSettings: { model, temperature, max_tokens },
+ currentActiveExtensionProcessingId: activeExtensionProcessingId,
+ newRequestBehaviorSetting: newRequestBehavior
+ }).catch(err => console.error("ADMIN_LOG_ERROR:", err));
+
+ if (activeConnections.length === 0) {
+ console.log(`SERVER.TS: Request ${requestId} - No extension. Responding 503.`);
+ logAdminMessage('CHAT_REQUEST_ERROR', requestId, { reason: "No extension" }).catch(err => console.error("ADMIN_LOG_ERROR:", err));
+ if (!res.headersSent) res.status(503).json({ error: { message: "No browser extension connected." } });
+ return;
+ }
+
+ if (activeExtensionProcessingId !== null) {
+ if (newRequestBehavior === 'drop') {
+ console.log(`SERVER.TS: Request ${requestId} dropped (extension busy with ${activeExtensionProcessingId}).`);
+ logAdminMessage('CHAT_REQUEST_DROPPED', requestId, { reason: "Extension busy" }).catch(err => console.error("ADMIN_LOG_ERROR:", err));
+ if (!res.headersSent) res.status(429).json({ error: { message: "Too Many Requests: Extension busy." } });
+ return;
+ } else {
+ requestQueue.push(queuedItem);
+ console.log(`SERVER.TS: Request ${requestId} queued. Position: ${requestQueue.length}.`);
+ logAdminMessage('CHAT_REQUEST_QUEUED', requestId, { queuePosition: requestQueue.length }).catch(err => console.error("ADMIN_LOG_ERROR:", err));
+ return;
+ }
+ }
+
+ activeExtensionProcessingId = requestId;
+ const extension = activeConnections[0];
+ activeExtensionSocketId = (extension as any).socketId;
+ console.log(`SERVER.TS: Processing request ${requestId} directly. ActiveID: ${activeExtensionProcessingId}, Assigned to Extension Socket: ${activeExtensionSocketId}`);
+ logAdminMessage('CHAT_REQUEST_PROCESSING', requestId, { status: 'Sending to extension', socketId: activeExtensionSocketId }).catch(err => console.error("ADMIN_LOG_ERROR:", err));
try {
- if (activeConnections.length === 0) {
- console.error(`SERVER: No active extension connection for request ${requestId} during processing.`);
- logAdminMessage('CHAT_REQUEST_ERROR', requestId, {
- reason: "No extension connected during processing attempt",
- activeExtensionProcessingId
- }).catch(err => console.error("ADMIN_LOG_ERROR (CHAT_REQUEST_ERROR):", err));
- if (!res.headersSent) {
- res.status(503).json({
- error: {
- message: "No browser extension connected when attempting to process request.",
- type: "server_error",
- code: "no_extension_during_processing"
- }
- });
- }
- return; // Exit early, finally block will call finishProcessingRequest
- }
-
const responsePromise = new Promise((resolve, reject) => {
pendingRequests.set(requestId, { resolve, reject });
setTimeout(() => {
if (pendingRequests.has(requestId)) {
+ const timedOutRequest = pendingRequests.get(requestId);
+ if (timedOutRequest) {
+ console.log(`SERVER.TS: Request ${requestId} timed out after ${currentRequestTimeoutMs}ms.`);
+ timedOutRequest.reject(new Error(`Request ${requestId} timed out`));
+ }
pendingRequests.delete(requestId);
- console.log(`SERVER: Request ${requestId} timed out after ${currentRequestTimeoutMs}ms during active processing. Rejecting promise.`);
- reject(new Error('Request timed out during active processing'));
+
+ logAdminMessage('REQUEST_CANCELLED_TIMEOUT', requestId, {
+ reason: `Timed out after ${currentRequestTimeoutMs}ms`,
+ socketId: activeExtensionSocketId
+ }).catch(err => console.error("ADMIN_LOG_ERROR:", err));
+
+ if (activeExtensionProcessingId === requestId) {
+ console.log(`SERVER.TS: Timed out request ${requestId} was the active one. Clearing active state.`);
+ activeExtensionProcessingId = null;
+ activeExtensionSocketId = null;
+ processNextInQueue();
+ }
}
}, currentRequestTimeoutMs);
});
- const extension = activeConnections[0];
- const messageToExtension: WebSocketMessage = {
- type: 'SEND_CHAT_MESSAGE',
- requestId,
- message: userMessage,
- settings: { model, temperature, max_tokens }
- };
-
+ // const extension = activeConnections[0]; // Moved up to set activeExtensionSocketId
+ const messageToExtension: WebSocketMessage = { type: 'SEND_CHAT_MESSAGE', requestId, message: userMessage, settings: { model, temperature, max_tokens } };
extension.send(JSON.stringify(messageToExtension));
- console.log(`SERVER: Request ${requestId} sent to browser extension via processRequest.`);
+ console.log(`SERVER.TS: Request ${requestId} sent to extension.`);
const responseData = await responsePromise;
-
const formattedResponse = {
- id: `chatcmpl-${Date.now()}`,
- object: "chat.completion",
- created: Math.floor(Date.now() / 1000),
+ id: `chatcmpl-${Date.now()}`, object: "chat.completion", created: Math.floor(Date.now() / 1000),
model: model || "relay-model",
choices: [{ index: 0, message: { role: "assistant", content: responseData }, finish_reason: "stop" }],
usage: { prompt_tokens: -1, completion_tokens: -1, total_tokens: -1 }
};
-
- logAdminMessage('CHAT_RESPONSE_SENT', requestId, {
- fromExtension: responseData,
- toClient: formattedResponse,
- status: "Success (processed)"
- }).catch(err => console.error("ADMIN_LOG_ERROR (CHAT_RESPONSE_SENT):", err));
- console.log(`SERVER: Request ${requestId} - Sending formatted response to client from processRequest.`);
-
- if (!res.headersSent) {
- res.json(formattedResponse);
- }
+ logAdminMessage('CHAT_RESPONSE_SENT', requestId, { fromExtension: responseData, toClient: formattedResponse, status: "Success" }).catch(err => console.error("ADMIN_LOG_ERROR:", err));
+ if (!res.headersSent) res.json(formattedResponse);
} catch (error: any) {
- console.error(`SERVER: Error in processRequest for ${requestId}:`, error);
- const errorResponseJson = {
- message: error.message || "Unknown error during request processing.",
- type: "server_error",
- code: error.message === 'Request timed out during active processing' ? "timeout_during_processing" : "processing_error",
- requestId
- };
- logAdminMessage('CHAT_ERROR_RESPONSE_SENT', requestId, {
- toClientError: errorResponseJson,
- status: `Error in processRequest: ${error.message}`
- }).catch(err => console.error("ADMIN_LOG_ERROR (CHAT_ERROR_RESPONSE_SENT):", err));
-
- if (!res.headersSent) {
- res.status(500).json({ error: errorResponseJson });
- }
+ console.error(`SERVER.TS: Error processing request ${requestId}:`, error);
+ logAdminMessage('CHAT_ERROR_RESPONSE_SENT', requestId, { toClientError: { message: error.message }, status: "Error" }).catch(err => console.error("ADMIN_LOG_ERROR:", err));
+ if (!res.headersSent) res.status(500).json({ error: { message: error.message || "Error processing request." } });
} finally {
- finishProcessingRequest(requestId);
+ const currentActiveId = activeExtensionProcessingId; // Capture current state before any changes
+
+ if (currentActiveId === requestId) {
+ // This path is typically hit if the request timed out on the server-side
+ // before the extension sent any final message, and no other handler (ws.onmessage, ws.onclose)
+ // has cleared the active ID for this specific request yet.
+ console.log(`SERVER.TS: Request ${requestId} (which was active) finalizing. Clearing active state in 'finally' block.`);
+ activeExtensionProcessingId = null;
+ activeExtensionSocketId = null;
+ } else if (currentActiveId === null) {
+ // This means activeExtensionProcessingId was already cleared, likely by the WebSocket 'onmessage'
+ // handler (if extension sent a response/error) or by the 'onclose' handler (if extension disconnected).
+ console.log(`SERVER.TS: Request ${requestId} finalizing. Active processing ID was already null (cleared by ws.onmessage or ws.onclose).`);
+ } else { // currentActiveId is not null AND not equal to requestId
+ console.log(`SERVER.TS: Request ${requestId} finalizing, but a different request (${currentActiveId}) is currently active. No change to active state for ${currentActiveId}.`);
+ }
+
+ pendingRequests.delete(requestId);
+ console.log(`SERVER.TS: Request ${requestId} removed from pendingRequests map. Calling processNextInQueue.`);
+ processNextInQueue();
}
}
-function finishProcessingRequest(completedRequestId: number): void {
- activeExtensionProcessingId = null;
- pendingRequests.delete(completedRequestId);
- console.log(`SERVER: Processing finished for requestId: ${completedRequestId}. Extension is now free.`);
-
- if (newRequestBehavior === 'queue' && requestQueue.length > 0) {
+function processNextInQueue() {
+ if (activeExtensionProcessingId === null && requestQueue.length > 0) {
const nextRequest = requestQueue.shift();
if (nextRequest) {
- console.log(`SERVER: Dequeuing request ${nextRequest.requestId}. Queue length: ${requestQueue.length}`);
- logAdminMessage('CHAT_REQUEST_DEQUEUED', nextRequest.requestId, {
- queueLength: requestQueue.length,
- dequeuedRequestId: nextRequest.requestId
- }).catch(err => console.error("ADMIN_LOG_ERROR (CHAT_REQUEST_DEQUEUED):", err));
-
- processRequest(nextRequest).catch((error: Error) => {
- console.error(`SERVER: Error processing dequeued request ${nextRequest.requestId}:`, error);
- if (!nextRequest.res.headersSent) {
- nextRequest.res.status(500).json({
- error: {
- message: `Failed to process dequeued request: ${error.message || 'Unknown error'}`,
- type: "server_error",
- code: "dequeued_request_processing_failed",
- requestId: nextRequest.requestId
- }
- });
- }
- });
+ console.log(`SERVER.TS: Dequeuing request ${nextRequest.requestId}. Queue length: ${requestQueue.length}`);
+ logAdminMessage('CHAT_REQUEST_DEQUEUED', nextRequest.requestId, { queueLength: requestQueue.length }).catch(err => console.error("ADMIN_LOG_ERROR:", err));
+ processOrQueueRequest(nextRequest).catch(e => console.error("Error from dequeued processOrQueueRequest:", e));
}
}
}
-// Create API router
const apiRouter: Router = express.Router();
-// OpenAI-compatible chat completions endpoint
+
apiRouter.post('/chat/completions', async (req: Request, res: Response): Promise => {
const requestId = requestCounter++;
const { messages, model, temperature, max_tokens } = req.body;
const userMessage = messages[messages.length - 1].content;
-
- // Log initial receipt and intended action
- let initialActionLog = 'DirectProcessing';
- if (activeExtensionProcessingId !== null) {
- initialActionLog = newRequestBehavior === 'queue' ? 'AttemptQueue' : 'AttemptDrop';
- }
- logAdminMessage('CHAT_REQUEST_RECEIVED', requestId, {
- fromClient: userMessage,
- modelSettings: { model, temperature, max_tokens },
- initialAction: initialActionLog, // Use the determined log value
- currentActiveExtensionProcessingId: activeExtensionProcessingId,
- newRequestBehaviorSetting: newRequestBehavior
- }).catch(err => console.error("ADMIN_LOG_ERROR (CHAT_REQUEST_RECEIVED):", err));
- console.log(`SERVER: Request ${requestId} received. Initial Action: ${initialActionLog}. Active ID: ${activeExtensionProcessingId}, Behavior: ${newRequestBehavior}`);
-
- if (activeConnections.length === 0) {
- logAdminMessage('CHAT_REQUEST_ERROR', requestId, {
- reason: "No extension connected at time of request",
- clientMessage: userMessage,
- details: "Response 503 sent to client."
- }).catch(err => console.error("ADMIN_LOG_ERROR (CHAT_REQUEST_ERROR):", err));
- console.log(`SERVER: Request ${requestId} - No browser extension connected. Responding 503.`);
- if (!res.headersSent) {
- res.status(503).json({
- error: {
- message: "No browser extension connected. Please open the chat interface and ensure the extension is active.",
- type: "server_error",
- code: "no_extension_connected"
- }
- });
- }
- return;
- }
-
- const queuedItem: QueuedRequest = {
- requestId,
- req,
- res,
- userMessage,
- model,
- temperature,
- max_tokens
- };
-
- if (activeExtensionProcessingId !== null) { // Extension is busy
- if (newRequestBehavior === 'drop') {
- logAdminMessage('CHAT_REQUEST_DROPPED', requestId, {
- reason: "Extension busy",
- droppedForRequestId: activeExtensionProcessingId,
- clientMessage: userMessage,
- details: "Response 429 sent to client."
- }).catch(err => console.error("ADMIN_LOG_ERROR (CHAT_REQUEST_DROPPED):", err));
- console.log(`SERVER: Request ${requestId} dropped as extension is busy with ${activeExtensionProcessingId}.`);
+ const queuedItem: QueuedRequest = { requestId, req, res, userMessage, model, temperature, max_tokens };
+ processOrQueueRequest(queuedItem).catch(e => {
+ console.error(`SERVER.TS: Unhandled error from processOrQueueRequest in /chat/completions for ${requestId}:`, e);
if (!res.headersSent) {
- res.status(429).json({
- error: {
- message: "Too Many Requests: Extension is currently busy. Please try again later.",
- type: "client_error",
- code: "extension_busy_request_dropped"
- }
- });
+ res.status(500).json({ error: { message: "Internal server error handling your request." } });
}
- return;
- }
-
- if (newRequestBehavior === 'queue') {
- requestQueue.push(queuedItem);
- logAdminMessage('CHAT_REQUEST_QUEUED', requestId, {
- queuePosition: requestQueue.length,
- queuedForRequestId: activeExtensionProcessingId,
- clientMessage: userMessage,
- queueLength: requestQueue.length
- }).catch(err => console.error("ADMIN_LOG_ERROR (CHAT_REQUEST_QUEUED):", err));
- console.log(`SERVER: Request ${requestId} queued. Position: ${requestQueue.length}. Extension busy with: ${activeExtensionProcessingId}`);
- // Do NOT send a response yet, the 'res' object is stored in the queue.
- return;
- }
- }
-
- // If extension is free (activeExtensionProcessingId is null)
- // processRequest will handle its own errors and responses including calling res.json() or res.status().json()
- processRequest(queuedItem).catch(error => {
- // 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}`
- }).catch(err => console.error("ADMIN_LOG_ERROR (CHAT_ERROR_RESPONSE_SENT):", err));
- if (!res.headersSent) {
- res.status(500).json({
- error: {
- message: `Internal server error during request processing: ${(error as Error).message || 'Unknown error'}`,
- type: "server_error",
- code: "unhandled_processing_error_in_handler",
- requestId: requestId
- }
- });
- }
});
});
-// Models endpoint
-apiRouter.get('/models', (req: Request, res: Response, next: NextFunction) => {
- try {
- res.json({
- object: "list",
- data: [
- {
- id: "gemini-pro",
- object: "model",
- created: 1677610602,
- owned_by: "relay"
- },
- {
- id: "claude-3",
- object: "model",
- created: 1677610602,
- owned_by: "relay"
- }
- ]
- });
- } catch (error) {
- next(error);
- }
+
+apiRouter.get('/models', (req: Request, res: Response) => {
+ res.json({
+ object: "list",
+ data: [
+ { id: "gemini-pro", object: "model", created: 1677610602, owned_by: "relay" },
+ { id: "claude-3", object: "model", created: 1677610602, owned_by: "relay" }
+ ]
+ });
});
-// Endpoint to retrieve message history for Admin UI
-apiRouter.get('/admin/message-history', (req: Request, res: Response): void => { // No longer async
+apiRouter.get('/admin/message-history', (req: Request, res: Response): void => {
try {
- // Return the latest 100 entries (or fewer if less than 100 exist)
const historyToReturn = adminMessageHistory.slice(0, 100);
- res.json(historyToReturn); // Objects are already parsed
+ res.json(historyToReturn);
} 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'
- }
- });
- }
+ console.error('SERVER.TS: Error fetching message history:', error);
+ if (!res.headersSent) res.status(500).json({ error: 'Failed to retrieve message history' });
}
});
-// Endpoint to provide server configuration and status
apiRouter.get('/admin/server-info', (req: Request, res: Response): void => {
try {
const uptimeSeconds = Math.floor((Date.now() - serverStartTime) / 1000);
const serverInfo = {
- 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
+ port: serverPortForListen,
+ requestTimeoutMs: currentRequestTimeoutMs,
+ newRequestBehavior: newRequestBehavior,
+ pingIntervalMs: null,
connectedExtensionsCount: activeConnections.length,
uptimeSeconds: uptimeSeconds,
};
res.json(serverInfo);
} 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'
- }
- });
- }
+ console.error('SERVER.TS: Error fetching server info:', error);
+ if (!res.headersSent) res.status(500).json({ error: 'Failed to retrieve server info' });
}
});
-// Endpoint to restart the server
apiRouter.post('/admin/restart-server', (req: Request, res: Response): void => {
- console.log('ADMIN: Received request to restart server.');
- // Removed premature res.json() call that was here.
-
- // Log absolute paths for debugging
- const absoluteConfigPath = path.resolve(CONFIG_FILE_PATH);
- const absoluteTriggerPath = path.resolve(RESTART_TRIGGER_FILE_PATH);
- console.log(`ADMIN: Config file path (absolute): ${absoluteConfigPath}`);
- console.log(`ADMIN: Trigger file path (absolute): ${absoluteTriggerPath}`);
-
- try {
- // 1. Update and save server-config.json
- const configToSave = loadServerConfig();
- configToSave.lastRestartRequestTimestamp = Date.now();
- saveServerConfig(configToSave); // This function already has its own try/catch and logs
- console.log('ADMIN: Attempted to update server-config.json.');
-
- // 2. Explicitly touch/create the .triggerrestart file.
- try {
- fs.writeFileSync(RESTART_TRIGGER_FILE_PATH, Date.now().toString(), 'utf-8');
- console.log(`ADMIN: Successfully wrote to restart trigger file: ${absoluteTriggerPath}`);
- } catch (triggerFileError) {
- console.error(`ADMIN: FAILED to write restart trigger file at ${absoluteTriggerPath}:`, triggerFileError);
- }
- } catch (outerError) {
- // This catch is for errors in loadServerConfig or if saveServerConfig itself throws unexpectedly
- console.error('ADMIN: Error in outer try block during restart sequence (e.g., loading config):', outerError);
- }
-
- // 3. Send response to client
- res.status(200).json({ message: 'Server restart initiated. Nodemon should pick up file changes.' });
-
- // 4. Exit the process after a longer delay.
- setTimeout(() => {
- console.log('ADMIN: Exiting process for nodemon to restart.');
- process.exit(0);
- }, 1500); // Increased delay to 1.5 seconds
+ console.log('SERVER.TS: Received request to restart server via /v1/admin/restart-server.');
+ res.status(200).json({ message: 'Restart should be handled by nodemon if .env file changes.' });
});
-// 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, autoKillPort: newAutoKillPortValue } = req.body;
- let configChanged = false;
- let messages: string[] = [];
-
- const currentConfig = loadServerConfig(); // Load current disk config to preserve other settings
-
- if (requestTimeoutMs !== undefined) {
- const newTimeout = parseInt(String(requestTimeoutMs), 10);
- if (!isNaN(newTimeout) && newTimeout > 0) {
- currentRequestTimeoutMs = newTimeout; // Update in-memory value immediately
- currentConfig.requestTimeoutMs = newTimeout; // Update config for saving
- configChanged = true;
- messages.push(`Request timeout updated in memory to ${currentRequestTimeoutMs}ms. This change is effective immediately.`);
- logAdminMessage('SETTING_UPDATE', 'SERVER_CONFIG', { setting: 'requestTimeoutMs', value: currentRequestTimeoutMs })
- .catch(err => console.error("ADMIN_LOG_ERROR (SETTING_UPDATE):", err));
- } else {
- res.status(400).json({ error: 'Invalid requestTimeoutMs value. Must be a positive number.' });
- return;
- }
- }
-
- if (port !== undefined) {
- const newPort = parseInt(String(port), 10);
- if (!isNaN(newPort) && newPort > 0 && newPort <= 65535) {
- 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 })
- .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.' });
- return;
- }
- }
-
- if (newBehaviorValue !== undefined) {
- if (newBehaviorValue === 'queue' || newBehaviorValue === 'drop') {
- newRequestBehavior = newBehaviorValue; // Update in-memory value immediately
- currentConfig.newRequestBehavior = newBehaviorValue; // Update config for saving
- configChanged = true;
- messages.push(`New request behavior updated to '${newRequestBehavior}'. This change is effective immediately.`);
- logAdminMessage('SETTING_UPDATE', 'SERVER_CONFIG', { setting: 'newRequestBehavior', value: newRequestBehavior })
- .catch(err => console.error("ADMIN_LOG_ERROR (SETTING_UPDATE newRequestBehavior):", err));
- } else {
- res.status(400).json({ error: "Invalid newRequestBehavior value. Must be 'queue' or 'drop'." });
- return;
- }
- }
-
- 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(' ') });
- } else {
- res.status(400).json({ error: 'No valid settings provided or no changes made.' });
- }
-});
-
-// Health check endpoint
-app.get('/health', (req: Request, res: Response) => {
- res.status(200).json({ status: 'OK', activeBrowserConnections: activeConnections.length });
-});
-
-// 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
-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);
+app.get('/health', (req: Request, res: Response) => {
+ const aliveConnections = activeConnections.filter(conn => conn.readyState === WebSocket.OPEN);
+ res.status(200).json({
+ status: 'ok',
+ timestamp: new Date().toISOString(),
+ activeBrowserConnections: aliveConnections.length,
+ totalTrackedBrowserConnections: activeConnections.length,
+ webSocketServerState: wss.options.server?.listening ? 'listening' : 'not_listening'
+ });
});
+
+server.listen(serverPortForListen, () => {
+ console.log(`SERVER.TS: OpenAI-compatible relay server started and listening on port ${serverPortForListen}`);
+ console.log(`SERVER.TS: WebSocket server for browser extensions running on ws://localhost:${serverPortForListen}`);
+ console.log(`SERVER.TS: Admin UI should be available at http://localhost:${serverPortForListen}/admin/admin.html`);
+});
+
+export default server;
diff --git a/api-relay-server/tsconfig.json b/api-relay-server/tsconfig.json
index bd3feae..f1b423f 100644
--- a/api-relay-server/tsconfig.json
+++ b/api-relay-server/tsconfig.json
@@ -8,7 +8,8 @@
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
- "forceConsistentCasingInFileNames": true
+ "forceConsistentCasingInFileNames": true,
+ "allowJs": true // Add this line
},
"include": [
"src/**/*"
diff --git a/extension/background.js b/extension/background.js
index fdc947c..fc16a48 100644
--- a/extension/background.js
+++ b/extension/background.js
@@ -1,7 +1,27 @@
+/*
+ * 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;
@@ -9,783 +29,784 @@ let reconnectInterval = 5000;
let reconnectTimer = null;
let activeTabId = null;
let serverUrl = '';
-let lastRequestId = null;
-let processingRequest = false;
-let pendingRequests = [];
-let lastSuccessfullyProcessedMessageText = null;
-const pendingRequestDetails = new Map();
-let currentRequestTargetTabId = null;
+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
+// 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();
+let debuggerAttachedTabs = new Map(); // tabId -> { providerName, patterns, isFetchEnabled, isAttached, lastKnownRequestId }
+// 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);
});
}
-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) {
- 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);
+ 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.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);
+ // Notify the server that this extension instance is ready
+ if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
+ const readyMessage = { type: 'EXTENSION_READY' };
+ relaySocket.send(JSON.stringify(readyMessage));
+ console.log("BACKGROUND: Relay WS: Sent EXTENSION_READY to server.");
+ }
+ };
- 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}`);
+ 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}`);
- pendingRequests.push(command);
- console.log(`BACKGROUND: Added command with requestId: ${command.requestId} to queue. Queue length: ${pendingRequests.length}`);
+ // 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);
+ }
+ };
- 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.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);
- }
+ 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);
+ }
}
-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;
- }
+// 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"));
} else {
- console.log(`BACKGROUND: Content script in tab ${targetTabIdForCommand} acknowledged command:`, response);
+ console.log(`BACKGROUND: Ping to stored tab ${activeTabId} successful.`);
+ targetTabIdForCommand = activeTabId;
+ resolve();
}
});
+ });
+ } 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 {
- 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).`);
- }
-
- if (lastRequestId === command.requestId) {
- processingRequest = false;
- currentRequestTargetTabId = null;
- console.log(`BACKGROUND: Reset processingRequest and currentRequestTargetTabId for requestId: ${command.requestId} (no suitable tab).`);
- }
- processNextRequest();
+ console.warn(BG_LOG_PREFIX, `Tab ${targetTabIdForCommand} is not being debugged. Cannot associate requestId for debugger.`);
}
- } catch (error) {
- console.error("BACKGROUND: Error in forwardCommandToContentScript for requestId:", command.requestId, error);
+ // Now actually send the command
+ chrome.tabs.sendMessage(targetTabIdForCommand, command, (response) => {
+ if (chrome.runtime.lastError) {
+ const errorMessage = `Error sending message to content script in tab ${targetTabIdForCommand}: ${chrome.runtime.lastError.message}`;
+ console.error(`BACKGROUND: ${errorMessage}`);
+
+ // Send error to server
+ if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
+ relaySocket.send(JSON.stringify({
+ type: "CHAT_RESPONSE_ERROR",
+ requestId: command.requestId,
+ error: `Failed to send command to content script (tab ${targetTabIdForCommand}). Detail: ${chrome.runtime.lastError.message}`
+ }));
+ console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR to server for requestId: ${command.requestId} (content script send failed).`);
+ } else {
+ console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE_ERROR for requestId: ${command.requestId} (content script send failed).`);
+ }
+
+ if (lastRequestId === command.requestId) {
+ processingRequest = false;
+ console.log(`BACKGROUND: Reset processingRequest for requestId: ${command.requestId} (content script send failed).`);
+ }
+ // Attempt to process the next request in the queue as this one failed at the background script level
+ processNextRequest();
+
+ } else {
+ console.log(`BACKGROUND: Content script in tab ${targetTabIdForCommand} acknowledged command:`, response);
+ // If content script acknowledges, it's responsible for sending a response/error back.
+ // No need to call processNextRequest() here; content script's response will trigger it.
+ }
+ });
+
+ } else {
+ 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: `Internal error in background script while forwarding command: ${error.message}`
+ error: errorMsg
}));
- console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR to server for requestId: ${command.requestId} (exception).`);
+ 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} (exception).`);
+ console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE_ERROR for requestId: ${command.requestId} (no suitable tab).`);
}
if (lastRequestId === command.requestId) {
processingRequest = false;
- currentRequestTargetTabId = null;
- console.log(`BACKGROUND: Reset processingRequest and currentRequestTargetTabId for requestId: ${command.requestId} (exception).`);
+ console.log(`BACKGROUND: Reset processingRequest for requestId: ${command.requestId} (no suitable tab).`);
}
- }
-}
-
-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;
-
- 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;
- }
-}
-
-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;
+ // Ensure processNextRequest is called to handle any queued items,
+ // even if this one failed.
+ processNextRequest();
}
- 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);
+ } 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.log("BACKGROUND: No pending requests to process.");
+ 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;
+
+ } 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;
+ }
+}
+
+// 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.`);
+ }
+
+ 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.");
+ }
+}
+
+// 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");
}
- if (providerName === "ChatGptProvider") {
+ // For ChatGPTProvider:
+ if (providerName === "ChatGptProvider") { // Match the casing used by the provider's .name property
return url.includes("chatgpt.com");
}
- if (providerName === "ClaudeProvider") {
+ // For ClaudeProvider:
+ if (providerName === "ClaudeProvider") { // Match the casing used by the provider's .name property
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.`);
- 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.`);
- }
- }
- }
+ 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.`);
+ }
+ }
}
+ }
- 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.`);
+ // 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);
+ 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 (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);
}
+ } 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);
+ }
+ // 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');
+ 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 (sender.tab && sender.tab.id) {
- activeTabId = sender.tab.id;
- 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();
+ }
+
+ } 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;
+ }
}
+ 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 (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;
- } 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_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." });
+ }
+ // 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;
+ } 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;
+
+ 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: message.response,
+ 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
}));
- sendResponse({ success: true });
-
- if (lastRequestId === message.requestId) {
- processingRequest = false;
- console.log("BACKGROUND: Reset processingRequest after (OLD DOM) RESPONSE_CAPTURED.");
- processNextRequest();
- }
-
+ console.log(`BACKGROUND: Sent CHAT_RESPONSE (for duplicate) to server for requestId: ${message.requestId}.`);
} 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;
- }
- }
- 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;
-
- 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." });
- }
- 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.`);
+ console.error(`BACKGROUND: Relay WS not OPEN, cannot send CHAT_RESPONSE (for duplicate) for requestId: ${message.requestId}.`);
}
- 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}` });
- }
+ 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;
+ }
+
+ 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
+ }));
+ }
+ }
+ 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);
} else {
- 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}` });
- }
+ console.log(`BACKGROUND: Sent STOP_STREAMING to tab ${activeTabId} for requestId ${lastRequestId}. Content script ack:`, response);
}
- } 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: 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",
- 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}.`);
- }
-
- processingRequest = false;
- currentRequestTargetTabId = null;
- const tabInfo = sender.tab ? debuggerAttachedTabs.get(sender.tab.id) : null;
- if (tabInfo && tabInfo.lastKnownRequestId === message.requestId) {
- tabInfo.lastKnownRequestId = 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 (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
- }));
- }
- }
- 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();
- }
-
+ 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.
if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
relaySocket.send(JSON.stringify({
type: "CHAT_RESPONSE_ERROR",
requestId: lastRequestId,
- error: "Request cancelled by user."
+ error: "Request cancelled by user (no active tab to signal provider)."
}));
- console.log(`BACKGROUND: Sent CHAT_RESPONSE_ERROR (user cancelled) to server for currently processing requestId: ${lastRequestId}.`);
}
+ // 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
+ }
- sendResponse({ success: true, message: `Stop initiated for currently processing request ${lastRequestId}. Waiting for finalization from content script.` });
+ // 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}.`);
+ }
+
+ // 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
+
+ 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;
-
- } 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." });
- }
- return 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." });
+ }
+ 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: {},
- registerProvider: function (name, domains, instance) {
+ _providers: {}, // providerName -> { instance, domains }
+ 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;
@@ -793,7 +814,10 @@ const providerUtils = {
}
return null;
},
- _initializeSimulatedProviders: function () {
+ // 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() {
this.registerProvider("AIStudioProvider", ["aistudio.google.com"], { name: "AIStudioProvider" });
this.registerProvider("GeminiProvider", ["gemini.google.com"], { name: "GeminiProvider" });
this.registerProvider("GeminiProvider", ["chatgpt.com"], { name: "ChatGPTProvider" });
@@ -801,11 +825,12 @@ const providerUtils = {
}
};
-providerUtils._initializeSimulatedProviders();
+providerUtils._initializeSimulatedProviders(); // Call to populate for the helper
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);
@@ -833,27 +858,47 @@ 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;
+ }
+
+ // Explicitly disable Fetch first, in case it's in a weird state
+ try {
+ console.log(BG_LOG_PREFIX, `Attempting to disable Fetch domain for tab ${tabId} before re-enabling.`);
+ await new Promise((resolve, reject) => {
+ chrome.debugger.sendCommand(debuggee, "Fetch.disable", {}, (disableResponse) => {
+ if (chrome.runtime.lastError) {
+ console.warn(BG_LOG_PREFIX, `Warning/Error disabling Fetch for tab ${tabId}:`, chrome.runtime.lastError.message);
+ // Don't reject, just log, as we want to proceed to enable anyway
+ } else {
+ console.log(BG_LOG_PREFIX, `Successfully disabled Fetch for tab ${tabId}. Response:`, disableResponse);
+ }
+ resolve();
+ });
+ });
+ } catch (e) {
+ console.warn(BG_LOG_PREFIX, `Exception during explicit Fetch.disable for tab ${tabId}:`, e);
}
console.log(BG_LOG_PREFIX, `Enabling Fetch domain for tab ${tabId} with patterns:`, patterns);
await new Promise((resolve, reject) => {
- chrome.debugger.sendCommand(debuggee, "Fetch.enable", { patterns: patterns }, () => {
+ const fetchEnableParams = { patterns: patterns };
+ console.log(BG_LOG_PREFIX, `Preparing to call Fetch.enable for tab ${tabId} with params:`, JSON.stringify(fetchEnableParams));
+ chrome.debugger.sendCommand(debuggee, "Fetch.enable", fetchEnableParams, (response) => {
if (chrome.runtime.lastError) {
console.error(BG_LOG_PREFIX, `Error enabling Fetch for tab ${tabId}:`, chrome.runtime.lastError.message);
return reject(chrome.runtime.lastError);
}
- console.log(BG_LOG_PREFIX, `Successfully enabled Fetch for tab ${tabId}`);
+ console.log(BG_LOG_PREFIX, `Successfully enabled Fetch for tab ${tabId}. Response from Fetch.enable:`, response);
const currentTabData = debuggerAttachedTabs.get(tabId);
if (currentTabData) {
currentTabData.isFetchEnabled = true;
@@ -863,9 +908,13 @@ async function attachDebuggerAndEnableFetch(tabId, providerName, patterns) {
});
} catch (error) {
console.error(BG_LOG_PREFIX, `Error in attachDebuggerAndEnableFetch for tab ${tabId}:`, error);
- const currentTabData = debuggerAttachedTabs.get(tabId);
- if (currentTabData && !currentTabData.isAttached) {
- debuggerAttachedTabs.delete(tabId);
+ // Ensure flags are reset on error
+ const currentTabDataOnError = debuggerAttachedTabs.get(tabId);
+ if (currentTabDataOnError) {
+ currentTabDataOnError.isFetchEnabled = false; // Ensure this is false
+ // if (!currentTabDataOnError.isAttached) { // Commenting out: if attach failed earlier, it's already deleted. If attach succeeded but enable failed, we want to keep attachment info.
+ // debuggerAttachedTabs.delete(tabId);
+ // }
}
}
}
@@ -884,14 +933,15 @@ async function detachDebugger(tabId) {
console.log(BG_LOG_PREFIX, `Successfully detached debugger from tab ${tabId}`);
}
debuggerAttachedTabs.delete(tabId);
- resolve();
+ resolve(); // Resolve even if detach had an error, as we've cleaned up map
});
});
- } catch (error) {
+ } catch (error) { // Catch errors from the Promise constructor itself or unhandled rejections
console.error(BG_LOG_PREFIX, `Exception during detach for tab ${tabId}:`, error);
- debuggerAttachedTabs.delete(tabId);
+ debuggerAttachedTabs.delete(tabId); // Ensure cleanup
}
} else {
+ // If not attached or no details, still ensure it's not in the map
debuggerAttachedTabs.delete(tabId);
}
}
@@ -920,6 +970,8 @@ 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);
@@ -932,16 +984,18 @@ 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.") &&
+ !textSegment.startsWith("1.") && // Avoid numbered list from thoughts
!textSegment.startsWith("2.") &&
!textSegment.startsWith("3.") &&
!textSegment.startsWith("4.") &&
!textSegment.startsWith("5.") &&
!textSegment.startsWith("6.") &&
textSegment.trim() !== "**") {
- combinedText += textSegment;
+ combinedText += textSegment; // Concatenate, newlines are part of the text
}
}
}
@@ -950,13 +1004,14 @@ 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));
@@ -966,122 +1021,145 @@ chrome.debugger.onEvent.addListener((debuggeeId, message, params) => {
}
if (message === "Fetch.requestPaused") {
- console.log(BG_LOG_PREFIX, `Fetch.requestPaused for tab ${tabId}, URL: ${params.request.url}, RequestId (debugger): ${params.requestId}, Stage: ${params.responseErrorReason || params.requestStage}`);
+ // Log immediately upon entering Fetch.requestPaused
+ console.log(BG_LOG_PREFIX, `ENTERED Fetch.requestPaused for tab ${tabId}, URL: ${params.request.url}, Debugger NetworkRequestId: ${params.requestId}, Stage: ${params.responseErrorReason || params.requestStage}, Headers:`, params.request.headers);
- if (!tabInfo || !tabInfo.isFetchEnabled) {
- console.log(BG_LOG_PREFIX, `Tab ${tabId} not actively monitored or Fetch not enabled. Continuing request.`);
+ if (!tabInfo) {
+ console.warn(BG_LOG_PREFIX, `Fetch.requestPaused for tab ${tabId} but NO tabInfo found in debuggerAttachedTabs. Continuing request.`);
+ chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
+ return;
+ }
+ if (!tabInfo.isFetchEnabled) {
+ console.log(BG_LOG_PREFIX, `Tab ${tabId} not actively monitored or Fetch not enabled (tabInfo.isFetchEnabled is false). Continuing request.`);
chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
return;
}
- if (params.responseStatusCode && params.responseStatusCode >= 200 && params.responseStatusCode < 300) {
- if (params.requestStage !== "Response") {
- console.warn(BG_LOG_PREFIX, `Proceeding to getResponseBody for tab ${tabId}, debugger requestId ${params.requestId}, even though requestStage is '${params.requestStage}' (expected 'Response'). Status: ${params.responseStatusCode}`);
+ // This is the application's requestId (e.g., 0, 1, 2...)
+ // It's CRITICAL that tabInfo.lastKnownRequestId is correctly set when the command was initially forwarded.
+ const currentOperationRequestId = tabInfo.lastKnownRequestId;
+
+ if (currentOperationRequestId === null || currentOperationRequestId === undefined) {
+ console.warn(BG_LOG_PREFIX, `Fetch.requestPaused for tab ${tabId} (URL: ${params.request.url}) but tabInfo.lastKnownRequestId is null/undefined. Cannot associate with an operation. Continuing request.`);
+ chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
+ return;
+ }
+
+ // Check if the URL matches any of the patterns for this tab
+ const matchesPattern = tabInfo.patterns.some(p => {
+ try {
+ // Ensure the pattern is treated as a string and properly escaped for regex construction.
+ // Basic wildcard to regex: replace * with .*? (non-greedy)
+ const patternRegex = new RegExp(String(p.urlPattern).replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*?'));
+ return patternRegex.test(params.request.url);
+ } catch (e) {
+ console.error(BG_LOG_PREFIX, `Error creating regex from pattern '${p.urlPattern}':`, e);
+ return false;
}
+ });
- chrome.debugger.sendCommand(debuggeeId, "Fetch.getResponseBody", { requestId: params.requestId }, (responseBodyData) => {
- let currentOperationRequestId = null;
- try {
- if (chrome.runtime.lastError) {
- console.error(BG_LOG_PREFIX, `Error calling Fetch.getResponseBody for tab ${tabId}, debugger requestId ${params.requestId}:`, chrome.runtime.lastError.message);
- return;
- }
- if (!responseBodyData) {
- console.error(BG_LOG_PREFIX, `Fetch.getResponseBody returned null or undefined for tab ${tabId}, debugger requestId ${params.requestId}.`);
- return;
- }
- console.log(BG_LOG_PREFIX, `Raw responseBodyData for debugger requestId ${params.requestId} (first 200 chars):`, JSON.stringify(responseBodyData).substring(0, 200) + "...");
+ if (!matchesPattern) {
+ // console.log(BG_LOG_PREFIX, `Fetch.requestPaused for URL ${params.request.url} did NOT match stored patterns for tab ${tabId}. Continuing request. Patterns:`, tabInfo.patterns);
+ chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
+ return;
+ }
+ console.log(BG_LOG_PREFIX, `Fetch.requestPaused for URL ${params.request.url} MATCHED pattern. App RequestId: ${currentOperationRequestId}. Proceeding...`);
+
+ console.log(BG_LOG_PREFIX, `[DEBUG_STAGE_STATUS] For matched URL ${params.request.url}, appReqId: ${currentOperationRequestId}, debugger netReqId: ${params.requestId}:`);
+ console.log(BG_LOG_PREFIX, ` - params.requestStage: ${params.requestStage}`);
+ console.log(BG_LOG_PREFIX, ` - params.responseStatusCode: ${params.responseStatusCode}`);
+ console.log(BG_LOG_PREFIX, ` - params.responseErrorReason: ${params.responseErrorReason}`);
- const rawBodyText = responseBodyData.base64Encoded ? new TextDecoder('utf-8').decode(Uint8Array.from(atob(responseBodyData.body), c => c.charCodeAt(0))) : responseBodyData.body;
+ // Additional pre-condition logging
+ console.log(BG_LOG_PREFIX, `[PRE-CONDITION CHECK] For AppReqId: ${currentOperationRequestId}, Debugger netReqId: ${params.requestId}`);
+ console.log(BG_LOG_PREFIX, ` - tabInfo (raw):`, tabInfo); // Log the raw tabInfo object
+ console.log(BG_LOG_PREFIX, ` - tabInfo.patterns:`, tabInfo ? JSON.stringify(tabInfo.patterns) : "tabInfo is null");
+ console.log(BG_LOG_PREFIX, ` - tabInfo.lastKnownRequestId: ${tabInfo ? tabInfo.lastKnownRequestId : "tabInfo is null"} (should be currentOperationRequestId)`);
+ console.log(BG_LOG_PREFIX, ` - currentOperationRequestId (derived from tabInfo.lastKnownRequestId): ${currentOperationRequestId}`);
- if (rawBodyText === undefined || rawBodyText === null) {
- console.error(BG_LOG_PREFIX, `Extracted rawBodyText is undefined or null for debugger requestId ${params.requestId}.`);
- return;
- }
-
- let tempRequestId = tabInfo ? tabInfo.lastKnownRequestId : null;
-
- 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;
- 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) {
- 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.`);
- } else {
- currentOperationRequestId = null;
- }
-
- 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));
-
- 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]"}'`);
-
- if (currentOperationRequestId !== null && currentOperationRequestId !== undefined && pendingRequestDetails.has(currentOperationRequestId)) {
- if (tabId) {
- const messageToSend = {
- type: "DEBUGGER_RESPONSE",
- requestId: currentOperationRequestId,
- 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));
- chrome.tabs.sendMessage(tabId, messageToSend, response => {
- if (chrome.runtime.lastError || !response || !response.success) {
- const errorMessage = chrome.runtime.lastError ? chrome.runtime.lastError.message : (response && response.error ? response.error : "No response or success false from content script");
- console.error(BG_LOG_PREFIX, `Error sending/acking DEBUGGER_RESPONSE to tab ${tabId} (app requestId ${currentOperationRequestId}): ${errorMessage}`);
-
- if (relaySocket && relaySocket.readyState === WebSocket.OPEN) {
- relaySocket.send(JSON.stringify({
- type: "CHAT_RESPONSE_ERROR",
- requestId: currentOperationRequestId,
- error: `Failed to send/ack DEBUGGER_RESPONSE to content script for requestId ${currentOperationRequestId}: ${errorMessage}`
- }));
- }
- if (processingRequest && lastRequestId === currentOperationRequestId) {
- processingRequest = false;
- pendingRequestDetails.delete(currentOperationRequestId);
- const tabInfoForReset = debuggerAttachedTabs.get(tabId);
- if (tabInfoForReset && tabInfoForReset.lastKnownRequestId === currentOperationRequestId) {
- tabInfoForReset.lastKnownRequestId = null;
- }
- console.log(BG_LOG_PREFIX, `Reset processingRequest due to DEBUGGER_RESPONSE send/ack failure for requestId ${currentOperationRequestId}.`);
- processNextRequest();
- }
- } else {
- console.log(BG_LOG_PREFIX, `Successfully sent DEBUGGER_RESPONSE to tab ${tabId} (app requestId ${currentOperationRequestId}), content script ack:`, response);
- console.log(BG_LOG_PREFIX, `Debugger response acknowledged by content script for app requestId ${currentOperationRequestId}. Waiting for FINAL_RESPONSE_TO_RELAY from provider.`);
- }
- });
- }
- } else {
- 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 {
- console.log(BG_LOG_PREFIX, `[FINALLY] Continuing debugger request ${params.requestId}.`);
- chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId }, () => {
- if (chrome.runtime.lastError) {
- console.error(BG_LOG_PREFIX, `Error continuing request ${params.requestId} for tab ${tabId} in finally:`, chrome.runtime.lastError.message);
- }
- });
+ // Scenario 1: Network error before even getting a response status (e.g., DNS failure)
+ if (params.responseErrorReason) {
+ console.error(BG_LOG_PREFIX, `Response error for ${params.request.url} (debugger netReqId ${params.requestId}): ${params.responseErrorReason}. AppReqId: ${currentOperationRequestId}.`);
+ const messageToContent = {
+ type: "PROVIDER_DEBUGGER_EVENT", // Ensure this type is handled by content.js
+ detail: {
+ requestId: currentOperationRequestId,
+ networkRequestId: params.requestId,
+ error: `Network error: ${params.responseErrorReason}`,
+ isFinal: true
}
+ };
+ chrome.tabs.sendMessage(tabId, messageToContent, response => {
+ if (chrome.runtime.lastError) console.error(BG_LOG_PREFIX, `Error sending debugger error event (responseErrorReason) to content script for tab ${tabId}:`, chrome.runtime.lastError.message);
+ else console.log(BG_LOG_PREFIX, `Sent debugger error event (responseErrorReason) to content script for tab ${tabId}, appReqId: ${currentOperationRequestId}, ack:`, response);
});
+ chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
+ return;
+ }
+
+ // Scenario 2: We have response headers (indicated by params.responseStatusCode being present).
+ if (params.responseStatusCode) {
+ if (params.responseStatusCode >= 200 && params.responseStatusCode < 300) {
+ // SUCCESS: We have a 2xx status, attempt to get the response body.
+ console.log(BG_LOG_PREFIX, `Attempting Fetch.getResponseBody for ${params.request.url}, appReqId: ${currentOperationRequestId}, debugger netReqId: ${params.requestId} (Stage: ${params.requestStage}, Status: ${params.responseStatusCode})`);
+ chrome.debugger.sendCommand(debuggeeId, "Fetch.getResponseBody", { requestId: params.requestId }, (responseBodyData) => {
+ let errorMessageForContent = null;
+ if (chrome.runtime.lastError) {
+ errorMessageForContent = `Error calling Fetch.getResponseBody: ${chrome.runtime.lastError.message}`;
+ console.error(BG_LOG_PREFIX, `${errorMessageForContent} for tab ${tabId}, appReqId: ${currentOperationRequestId}, debugger netReqId: ${params.requestId}`);
+ }
+
+ console.log(BG_LOG_PREFIX, `[getResponseBody CB] appReqId: ${currentOperationRequestId}, netReqId: ${params.requestId}. responseBodyData raw:`, responseBodyData);
+ if (responseBodyData) {
+ console.log(BG_LOG_PREFIX, `[getResponseBody CB] responseBodyData.body (first 100): ${responseBodyData.body ? String(responseBodyData.body).substring(0,100) : 'N/A'}, .base64Encoded: ${responseBodyData.base64Encoded}`);
+ }
+ console.log(BG_LOG_PREFIX, `[getResponseBody CB] errorMessageForContent before check: '${errorMessageForContent}'`);
+
+ if (!responseBodyData && !errorMessageForContent) {
+ errorMessageForContent = "No response body data and no explicit error from getResponseBody.";
+ console.warn(BG_LOG_PREFIX, `${errorMessageForContent} for tab ${tabId}, appReqId: ${currentOperationRequestId}, debugger netReqId: ${params.requestId}`);
+ }
+ console.log(BG_LOG_PREFIX, `[getResponseBody CB] errorMessageForContent AFTER check: '${errorMessageForContent}'`);
+
+ const messageToContent = {
+ type: "PROVIDER_DEBUGGER_EVENT",
+ detail: {
+ requestId: currentOperationRequestId,
+ networkRequestId: params.requestId,
+ data: responseBodyData ? responseBodyData.body : null,
+ base64Encoded: responseBodyData ? responseBodyData.base64Encoded : false,
+ error: errorMessageForContent,
+ isFinal: true
+ }
+ };
+ console.log(BG_LOG_PREFIX, `Sending PROVIDER_DEBUGGER_EVENT (body/error) to content script for tab ${tabId}, appReqId: ${currentOperationRequestId}. Error: ${errorMessageForContent}, Data (first 100): ${messageToContent.detail.data ? String(messageToContent.detail.data).substring(0,100) + "..." : "null"}`);
+ chrome.tabs.sendMessage(tabId, messageToContent, response => {
+ if (chrome.runtime.lastError) console.error(BG_LOG_PREFIX, `Error sending/acking debugger event (body/error) to content script for tab ${tabId}:`, chrome.runtime.lastError.message);
+ else console.log(BG_LOG_PREFIX, `Sent debugger event (body/error) to content script for tab ${tabId}, appReqId: ${currentOperationRequestId}, ack:`, response);
+ });
+ });
+ } else { // Non-2xx status code
+ const httpErrorMessage = `HTTP error ${params.responseStatusCode} for ${params.request.url}`;
+ console.error(BG_LOG_PREFIX, `${httpErrorMessage}. AppReqId: ${currentOperationRequestId}, Debugger netReqId: ${params.requestId}.`);
+ const messageToContent = {
+ type: "PROVIDER_DEBUGGER_EVENT",
+ detail: {
+ requestId: currentOperationRequestId,
+ networkRequestId: params.requestId,
+ error: httpErrorMessage,
+ isFinal: true
+ }
+ };
+ chrome.tabs.sendMessage(tabId, messageToContent, response => {
+ if (chrome.runtime.lastError) console.error(BG_LOG_PREFIX, `Error sending debugger HTTP error event to content script for tab ${tabId}:`, chrome.runtime.lastError.message);
+ else console.log(BG_LOG_PREFIX, `Sent debugger HTTP error event to content script for tab ${tabId}, appReqId: ${currentOperationRequestId}, ack:`, response);
+ });
+ chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
+ }
} else {
- console.log(BG_LOG_PREFIX, `Fetch.requestPaused for tab ${tabId} (debugger requestId ${params.requestId}). Not a capturable success response. Status: ${params.responseStatusCode}, ErrorReason: ${params.responseErrorReason}, Stage: ${params.requestStage}. Continuing request.`);
+ // No response headers yet (no params.responseStatusCode), and no responseErrorReason.
+ // This could be an earlier stage of the request or one not relevant for body capture.
+ console.log(BG_LOG_PREFIX, `Request for ${params.request.url} (debugger netReqId: ${params.requestId}, appReqId: ${currentOperationRequestId}) does not have responseStatusCode. Stage: ${params.requestStage}. Continuing request.`);
chrome.debugger.sendCommand(debuggeeId, "Fetch.continueRequest", { requestId: params.requestId });
}
}
diff --git a/extension/content.js b/extension/content.js
index f0fdb9e..98038b7 100644
--- a/extension/content.js
+++ b/extension/content.js
@@ -1,19 +1,41 @@
+/*
+ * 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");
-let provider = null;
+// Global state
+let provider = null; // This will be set by initializeContentRelay
let setupComplete = false;
let currentRequestId = null;
-let processingMessage = false;
-let responseMonitoringTimers = [];
-let captureAttempts = 0;
-const MAX_CAPTURE_ATTEMPTS = 30;
-const CAPTURE_DELAY = 1000;
+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
+// 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) => {
@@ -25,7 +47,8 @@ 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) => {
@@ -37,7 +60,8 @@ 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) => {
@@ -51,102 +75,109 @@ 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 (window.providerUtils) {
- const detectedProvider = window.providerUtils.detectProvider(window.location.hostname);
- provider = detectedProvider;
-
- 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 (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 (setupComplete) {
+ console.log(CS_LOG_PREFIX, "Initialization already attempted or complete.");
+ return;
}
- } else {
- console.error(CS_LOG_PREFIX, 'providerUtils not found. Cannot detect provider or send patterns.');
- }
+ console.log(CS_LOG_PREFIX, 'Initializing content relay...');
- 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);
+ // Provider Detection
+ if (window.providerUtils) {
+ const detectedProvider = window.providerUtils.detectProvider(window.location.hostname); // New detection method
+ provider = detectedProvider; // Update the global provider instance
+
+ 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 (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.');
+ }
+ }
} else {
- console.log(CS_LOG_PREFIX, 'CHAT_RELAY_READY message sent, response:', response);
+ 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 (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.");
+ // 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
+ } 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) {
- clearInterval(pollingInterval);
- console.warn(CS_LOG_PREFIX, "Stopping element polling: provider became unavailable.");
- return;
+ 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;
}
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);
@@ -154,15 +185,18 @@ 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;
+ processingMessage = false; // Reset flag
return false;
}
- return sendChatMessageWithRetry(text, 5);
+ // Try to send the message with retries
+ return sendChatMessageWithRetry(text, 5); // Try up to 5 times
}
+// 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.`);
@@ -178,13 +212,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})`);
@@ -193,31 +227,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;
- } 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; // 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);
+ }
} else {
- console.error(CS_LOG_PREFIX, "Provider reported failure sending message.");
- processingMessage = false;
+ console.error(CS_LOG_PREFIX, "Provider reported failure sending message.");
+ processingMessage = false; // Reset on failure
}
return result;
} catch (error) {
@@ -227,47 +261,49 @@ 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;
+ processingMessage = false; // Can't monitor, so reset.
return;
}
console.log(CS_LOG_PREFIX, "Starting response monitoring process...");
- captureAttempts = 0;
+ captureAttempts = 0; // Reset capture attempts for this new monitoring session
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;
+ return; // Stop if no longer processing a message
}
-
+
if (captureAttempts >= MAX_CAPTURE_ATTEMPTS) {
console.error(CS_LOG_PREFIX, "Maximum response capture attempts reached. Stopping monitoring.");
- 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);
- }
- });
+ // 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);
+ }
+ });
}
processingMessage = false;
- currentRequestId = null;
+ currentRequestId = null; // Clear current request ID as it timed out
return;
}
@@ -277,40 +313,45 @@ function startMonitoringForResponse() {
const responseElement = document.querySelector(provider.responseSelector);
if (responseElement) {
const responseText = provider.getResponseText(responseElement);
- const isFinal = provider.isResponseComplete ? provider.isResponseComplete(responseElement) : false;
+ const isFinal = provider.isResponseComplete ? provider.isResponseComplete(responseElement) : false; // Default to false if not implemented
console.log(CS_LOG_PREFIX, `Captured response text (length: ${responseText.length}), isFinal: ${isFinal}`);
-
+
+ // Send to background
chrome.runtime.sendMessage({
- type: "FINAL_RESPONSE_TO_RELAY",
+ type: "FINAL_RESPONSE_TO_RELAY", // Or a new type like "PARTIAL_RESPONSE" if needed
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;
- return;
+ processingMessage = false; // Reset flag as processing is complete
+ // currentRequestId will be cleared by handleProviderResponse or if a new message comes
+ 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.");
@@ -323,35 +364,47 @@ 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) {
- return;
+ // console.log(CS_LOG_PREFIX, "MutationObserver: Ignoring mutation, not actively processing a message or no currentRequestId.");
+ 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);
+ console.error(CS_LOG_PREFIX, "Failed to start MutationObserver:", e, "on target:", targetNode);
+ // Fallback or error handling if observer cannot be started
}
+ // 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.");
@@ -360,20 +413,35 @@ 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();
- captureResponse(null, true);
+ // 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);
}
+ // 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 {
@@ -381,116 +449,141 @@ function monitorResponseCompletion(element) {
}
}
+// Specific monitoring for Gemini, if needed (example)
function monitorGeminiResponse(element) {
- console.log(CS_LOG_PREFIX, "Monitoring Gemini response element:", element);
- const observer = new MutationObserver((mutationsList, obs) => {
- let isComplete = false;
+ // 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);
- }
- });
- observer.observe(element, { attributes: true, childList: true, subtree: true });
- console.log(CS_LOG_PREFIX, "Gemini response observer started.");
+ 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.");
}
function monitorGeminiContentStability(element) {
- let lastContent = "";
- let stableCount = 0;
- const STABLE_THRESHOLD = 3;
- const CHECK_INTERVAL = 300;
+ 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);
+ 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;
- }
+ 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);
- }
- }
+ 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);
+ }
+ }
- 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);
+ 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
}
+// 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);
+ 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;
}
- return;
- }
- responseElement = document.querySelector(provider.responseSelector);
- console.log(CS_LOG_PREFIX, "Using provider.responseSelector for capture:", provider.responseSelector);
+ 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));
- responseMonitoringTimers = [];
+ 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
}
-function setupMessageListeners() {
+// Define message listener function *before* calling it
+// Renamed setupAutomaticMessageSending to setupMessageListeners
+function setupMessageListeners() { // Renamed from setupAutomaticMessageSending
+ // Listen for commands from the background script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "SEND_CHAT_MESSAGE") {
- const messageContent = message.messageContent;
+ const messageContent = message.messageContent; // Use messageContent
let messagePreview = "";
if (typeof messageContent === 'string') {
messagePreview = `String: "${messageContent.substring(0, 50)}..."`;
@@ -501,9 +594,9 @@ function setupMessageListeners() {
} 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.");
@@ -511,6 +604,7 @@ function setupMessageListeners() {
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();
@@ -518,10 +612,11 @@ function setupMessageListeners() {
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;
@@ -529,18 +624,24 @@ function setupMessageListeners() {
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);
- if (currentRequestId === message.requestId) {
- processingMessage = false;
+ // 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
}
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) {
@@ -548,14 +649,16 @@ function setupMessageListeners() {
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 = '';
+ 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 }));
chrome.runtime.sendMessage({
type: "DUPLICATE_MESSAGE_HANDLED",
requestId: message.requestId,
- originalText: messageContent
+ originalText: messageContent // The text that was duplicated
}, response => {
if (chrome.runtime.lastError) {
console.error(CS_LOG_PREFIX, 'Error sending DUPLICATE_MESSAGE_HANDLED:', chrome.runtime.lastError.message);
@@ -564,22 +667,29 @@ function setupMessageListeners() {
}
});
+ // 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)
+ provider.sendChatMessage(messageContent, currentRequestId) // Pass messageContent and the requestId
.then(success => {
if (success) {
console.log(CS_LOG_PREFIX, `Message sending initiated successfully via provider for requestId: ${currentRequestId}.`);
@@ -588,13 +698,15 @@ function setupMessageListeners() {
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}.`);
- 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
+ // 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
});
- processingMessage = false;
+ processingMessage = false; // As we can't process response
currentRequestId = null;
}
sendResponse({ success: true, message: "Message sending initiated by provider." });
@@ -613,34 +725,42 @@ function setupMessageListeners() {
} else {
console.error(CS_LOG_PREFIX, "Provider or provider.sendChatMessage is not available for requestId:", message.requestId);
processingMessage = false;
- currentRequestId = null;
+ currentRequestId = null; // Ensure reset if it was about to be set
sendResponse({ success: false, error: "Provider or sendChatMessage method missing." });
}
- return true;
+ return true; // Indicate async response
} else if (message.type === "DEBUGGER_RESPONSE") {
- console.log(CS_LOG_PREFIX, "Received DEBUGGER_RESPONSE message object:", JSON.stringify(message));
+ console.log(CS_LOG_PREFIX, "Received DEBUGGER_RESPONSE message object:", JSON.stringify(message)); // Log full received 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;
- }
-
- 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);
- sendResponse({ success: true, message: "Debugger data passed to provider." });
- 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
+ const errorFromBackground = message.error || null; // Get error from message, default to null
+ console.log(CS_LOG_PREFIX, `Calling provider.handleDebuggerData for requestId: ${message.requestId} with isFinal: ${message.isFinal}, errorFromBackground: ${errorFromBackground}`); // Log before call
+ provider.handleDebuggerData(message.requestId, message.data, message.isFinal, errorFromBackground, handleProviderResponse); // Pass errorFromBackground
+ // Acknowledge receipt of the debugger data
+ sendResponse({ success: true, message: "Debugger data (and potential error) passed to provider." });
+ return true; // Indicate async response (provider will eventually call handleProviderResponse)
} else if (message.type === "PING_TAB") {
console.log(CS_LOG_PREFIX, "Received PING_TAB from background script.");
@@ -650,11 +770,13 @@ function setupMessageListeners() {
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;
- clearResponseMonitoringTimers();
- console.log(CS_LOG_PREFIX, `STOP_STREAMING: Cleared active currentRequestId ${message.requestId} and processingMessage flag.`);
+ 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.`);
}
sendResponse({ success: true, message: `Streaming stopped for requestId: ${message.requestId}` });
} else {
@@ -664,69 +786,79 @@ function setupMessageListeners() {
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}`);
-
- 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.`);
+ 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.`);
}
+ // Continue to process with the 'requestId' passed to this function.
if (chrome.runtime && chrome.runtime.sendMessage) {
- const MAX_RESPONSE_TEXT_LENGTH = 500 * 1024;
- let messageToSendToBackground;
+ const MAX_RESPONSE_TEXT_LENGTH = 500 * 1024; // 500KB limit for safety
+ let messageToSendToBackground;
- // 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
- };
- }
+ 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
+ };
+ }
- 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);
- }
+ 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();
+ clearResponseMonitoringTimers(); // Clear any timers associated with this request
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.`);
@@ -734,20 +866,22 @@ 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();
+ attemptInitialization(); // DOMContentLoaded has already fired
}
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();
- setupMessageListeners();
- 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(); // 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.");
}
diff --git a/extension/providers/claude.js b/extension/providers/claude.js
index 4056945..b41e237 100644
--- a/extension/providers/claude.js
+++ b/extension/providers/claude.js
@@ -294,20 +294,30 @@ class ClaudeProvider {
}
}
- handleDebuggerData(requestId, rawData, isFinalFromBackground) {
+ handleDebuggerData(requestId, rawData, isFinalFromBackground, errorFromBackground = null) {
// !!!!! VERY IMPORTANT ENTRY LOG !!!!!
- console.log('[[ClaudeProvider]] handleDebuggerData ENTERED. RequestId: ' + requestId + ', isFinalFromBackground: ' + isFinalFromBackground + ', RawData Length: ' + (rawData ? rawData.length : 'null'));
+ console.log('[[ClaudeProvider]] handleDebuggerData ENTERED. RequestId: ' + requestId + ', isFinalFromBackground: ' + isFinalFromBackground + ', RawData Length: ' + (rawData ? rawData.length : 'null') + ', ErrorFromBG: ' + errorFromBackground);
const callback = this.pendingResponseCallbacks.get(requestId);
if (!callback) {
- console.warn('[' + this.name + '] No pending callback for requestId: ' + requestId + '. Ignoring.');
+ console.warn('[' + this.name + '] No pending callback for requestId: ' + requestId + '. Ignoring debugger data/error.');
if (this.requestBuffers.has(requestId)) {
this.requestBuffers.delete(requestId);
}
return;
}
+ if (errorFromBackground) {
+ console.warn(`[${this.name}] handleDebuggerData: Propagating error for requestId ${requestId}: ${errorFromBackground}`);
+ callback(requestId, `[Provider Error from Background] ${errorFromBackground}`, true); // Pass error as text, mark as final
+ this.pendingResponseCallbacks.delete(requestId);
+ if (this.requestBuffers.has(requestId)) {
+ this.requestBuffers.delete(requestId); // Clean up buffer too
+ }
+ return; // Stop further processing
+ }
+
if (!this.requestBuffers.has(requestId)) {
this.requestBuffers.set(requestId, { accumulatedText: "" });
}
@@ -510,7 +520,7 @@ class ClaudeProvider {
console.log(`[${this.name}] getStreamingApiPatterns called. Capture method: ${this.captureMethod}`);
if (this.captureMethod === "debugger" && this.debuggerUrlPattern) {
console.log(`[${this.name}] Using debugger URL pattern: ${this.debuggerUrlPattern}`);
- return [{ urlPattern: this.debuggerUrlPattern, requestStage: "Response" }];
+ return [{ urlPattern: this.debuggerUrlPattern }];
}
console.log(`[${this.name}] No debugger patterns to return (captureMethod is not 'debugger' or no pattern set).`);
return [];
diff --git a/extension/providers/gemini.js b/extension/providers/gemini.js
index e697657..3bc56e0 100644
--- a/extension/providers/gemini.js
+++ b/extension/providers/gemini.js
@@ -22,58 +22,69 @@ class GeminiProvider {
this.name = 'GeminiProvider'; // Updated
this.supportedDomains = ['gemini.google.com'];
+ // --- START OF CONFIGURABLE PROPERTIES (similar to other providers) ---
+ this.captureMethod = "dom"; // Switched to DOM capture by default
+ // TODO: DEVELOPER ACTION REQUIRED! Verify this URL pattern with Gemini's actual network requests.
+ this.debuggerUrlPattern = "*StreamGenerate*"; // Kept for potential future switch
+ this.includeThinkingInMessage = false; // Gemini likely doesn't have a separate "thinking" stream like some others.
+ // --- END OF CONFIGURABLE PROPERTIES ---
+
// Selectors for the Gemini interface
this.inputSelector = 'div.ql-editor, div[contenteditable="true"], textarea[placeholder="Enter a prompt here"], textarea.message-input, textarea.input-area';
this.sendButtonSelector = 'button[aria-label="Send message"], button.send-button, button.send-message-button';
- // Response selector - updated to match the actual elements
- this.responseSelector = 'model-response, message-content, .model-response-text, .markdown-main-panel, .model-response, div[id^="model-response-message"]';
- // Thinking indicator selector
+ // Response selector - updated to the specific div for DOM capture
+ this.responseSelector = 'div.markdown.markdown-main-panel[id^="model-response-message-content"]';
+ // Thinking indicator selector - kept as is, assuming these are still relevant
this.thinkingIndicatorSelector = '.thinking-indicator, .loading-indicator, .typing-indicator, .response-loading, .blue-circle, .stop-icon';
- // Fallback selectors (NEW)
- this.responseSelectorForDOMFallback = 'model-response, message-content, .model-response-text, .markdown-main-panel'; // Placeholder
- this.thinkingIndicatorSelectorForDOM = '.thinking-indicator, .loading-indicator, .blue-circle, .stop-icon'; // Placeholder
+ // Fallback selectors are less relevant now that DOM is primary but kept for completeness
+ this.responseSelectorForDOMFallback = 'model-response, message-content, .model-response-text, .markdown-main-panel';
+ this.thinkingIndicatorSelectorForDOM = '.thinking-indicator, .loading-indicator, .blue-circle, .stop-icon';
// Last sent message to avoid capturing it as a response
this.lastSentMessage = '';
// Flag to prevent double-sending - IMPORTANT: This must be false by default
this.hasSentMessage = false;
+
+ // For debugger-based response capture
+ this.pendingResponseCallbacks = new Map();
+ this.requestAccumulators = new Map(); // To accumulate response chunks for a given request
}
// Send a message to the chat interface (MODIFIED)
async sendChatMessage(text) {
- console.log(`[${this.name}] sendChatMessage called with:`, text);
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] sendChatMessage called with:`, text);
const inputElement = document.querySelector(this.inputSelector);
const sendButton = document.querySelector(this.sendButtonSelector);
if (!inputElement || !sendButton) {
- console.error(`[${this.name}] Missing input field (${this.inputSelector}) or send button (${this.sendButtonSelector})`);
+ console.error(`[${this.name}] [${this.captureMethod.toUpperCase()}] Missing input field (${this.inputSelector}) or send button (${this.sendButtonSelector})`);
return false;
}
- console.log(`[${this.name}] Attempting to send message with:`, {
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Attempting to send message with:`, {
inputFieldInfo: inputElement.outerHTML.substring(0,100),
sendButtonInfo: sendButton.outerHTML.substring(0,100)
});
try {
this.lastSentMessage = text;
- console.log(`[${this.name}] Stored last sent message:`, this.lastSentMessage);
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Stored last sent message:`, this.lastSentMessage);
if (inputElement.tagName.toLowerCase() === 'div' && (inputElement.contentEditable === 'true' || inputElement.getAttribute('contenteditable') === 'true')) {
- console.log(`[${this.name}] Input field is a contentEditable div.`);
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Input field is a contentEditable div.`);
inputElement.focus();
inputElement.innerHTML = ''; // Clear existing content
inputElement.textContent = text; // Set the new text content
inputElement.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
- console.log(`[${this.name}] Set text content and dispatched input event for contentEditable div.`);
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Set text content and dispatched input event for contentEditable div.`);
} else { // Standard input or textarea
- console.log(`[${this.name}] Input field is textarea/input.`);
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Input field is textarea/input.`);
inputElement.value = text;
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
inputElement.focus();
- console.log(`[${this.name}] Set value and dispatched input event for textarea/input.`);
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Set value and dispatched input event for textarea/input.`);
}
await new Promise(resolve => setTimeout(resolve, 500)); // Preserved delay
@@ -83,261 +94,355 @@ class GeminiProvider {
sendButton.classList.contains('disabled');
if (!isDisabled) {
- console.log(`[${this.name}] Clicking send button.`);
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Clicking send button.`);
sendButton.click();
return true;
} else {
- console.warn(`[${this.name}] Send button is disabled. Cannot send message.`);
+ console.warn(`[${this.name}] [${this.captureMethod.toUpperCase()}] Send button is disabled. Cannot send message.`);
return false;
}
} catch (error) {
- console.error(`[${this.name}] Error sending message:`, error);
+ console.error(`[${this.name}] [${this.captureMethod.toUpperCase()}] Error sending message:`, error);
return false;
}
}
// Capture response from the chat interface (Original logic, logs updated for consistency)
captureResponse(element) {
+ // This method is called when this.captureMethod === "dom"
+ // 'element' is expected to be the one matching this.responseSelector
if (!element) {
- console.log(`[${this.name}] No element provided to captureResponse`);
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] No element provided to captureResponse (expected match for ${this.responseSelector})`);
return { found: false, text: '' };
}
- console.log(`[${this.name}] Attempting to capture response from Gemini:`, element);
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Attempting to capture response from Gemini element:`, element.outerHTML.substring(0, 200) + "...");
let responseText = "";
let foundResponse = false;
try {
- console.log(`[${this.name}] Looking for response in various elements...`);
-
+ // Primarily rely on the textContent of the matched element
if (element.textContent) {
- console.log(`[${this.name}] Element has text content`);
responseText = element.textContent.trim();
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Raw textContent from element (len ${responseText.length}): "${responseText.substring(0, 100)}..."`);
+ // Basic validation
if (responseText &&
responseText !== this.lastSentMessage &&
- !responseText.includes("Loading") &&
- !responseText.includes("Thinking") &&
+ !responseText.toLowerCase().includes("loading") && // Case-insensitive for common words
+ !responseText.toLowerCase().includes("thinking") &&
!responseText.includes("You stopped this response")) {
- console.log(`[${this.name}] Found response in element:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
+
+ // HTML entities like < are automatically decoded by textContent
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Valid response found:`, responseText.substring(0, 100) + (responseText.length > 100 ? "..." : ""));
foundResponse = true;
} else {
- console.log(`[${this.name}] Element text appears to be invalid:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
+ if (responseText === this.lastSentMessage) {
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Text content matches last sent message. Not a new response.`);
+ } else {
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Text content appears to be invalid or loading state: "${responseText.substring(0, 100)}..."`);
+ }
+ responseText = ""; // Clear if not a valid new response
}
} else {
- console.log(`[${this.name}] Element has no text content`);
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Element has no textContent.`);
}
- console.log(`[${this.name}] Trying to find the most recent conversation container...`);
-
- const conversationContainers = document.querySelectorAll('.conversation-container');
- if (conversationContainers && conversationContainers.length > 0) {
- console.log(`[${this.name}] Found ${conversationContainers.length} conversation containers`);
- const lastContainer = conversationContainers[conversationContainers.length - 1];
- console.log(`[${this.name}] Last container ID:`, lastContainer.id);
-
- const userQuery = lastContainer.querySelector('.user-query-container');
- const userText = userQuery ? userQuery.textContent.trim() : '';
-
- if (userText === this.lastSentMessage) {
- console.log(`[${this.name}] Found container with our last sent message, looking for response`);
- }
-
- const modelResponse = lastContainer.querySelector('model-response');
- if (modelResponse) {
- console.log(`[${this.name}] Found model-response in last conversation container`);
- const messageContent = modelResponse.querySelector('message-content.model-response-text');
- if (messageContent) {
- console.log(`[${this.name}] Found message-content in model-response`);
- const markdownDiv = messageContent.querySelector('.markdown');
- if (markdownDiv) {
- console.log(`[${this.name}] Found markdown div in message-content`);
- const text = markdownDiv.textContent.trim();
- if (text &&
- text !== this.lastSentMessage &&
- !text.includes("Loading") &&
- !text.includes("Thinking") &&
- !text.includes("You stopped this response")) {
- responseText = text;
- console.log(`[${this.name}] Found response in markdown div:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
- foundResponse = true;
- }
- }
- }
- }
- } else {
- console.log(`[${this.name}] No conversation containers found`);
- }
-
- if (!foundResponse) {
- console.log(`[${this.name}] Trying to find model-response-message-content elements...`);
- const responseMessages = document.querySelectorAll('div[id^="model-response-message-content"]');
- if (responseMessages && responseMessages.length > 0) {
- console.log(`[${this.name}] Found ${responseMessages.length} model-response-message-content elements`);
- const sortedMessages = Array.from(responseMessages).sort((a, b) => {
- return a.id.localeCompare(b.id);
- });
- const responseMessage = sortedMessages[sortedMessages.length - 1];
- console.log(`[${this.name}] Last response message ID:`, responseMessage.id);
- const text = responseMessage.textContent.trim();
- if (text &&
- text !== this.lastSentMessage &&
- !text.includes("Loading") &&
- !text.includes("Thinking") &&
- !text.includes("You stopped this response")) {
- responseText = text;
- console.log(`[${this.name}] Found response in model-response-message:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
- foundResponse = true;
- }
- } else {
- console.log(`[${this.name}] No model-response-message-content elements found`);
- }
- }
-
- if (!foundResponse) {
- console.log(`[${this.name}] Trying to find message-content elements...`);
- const messageContents = document.querySelectorAll('message-content.model-response-text');
- if (messageContents && messageContents.length > 0) {
- console.log(`[${this.name}] Found ${messageContents.length} message-content elements`);
- const sortedContents = Array.from(messageContents).sort((a, b) => {
- return (a.id || '').localeCompare(b.id || '');
- });
- const lastMessageContent = sortedContents[sortedContents.length - 1];
- console.log(`[${this.name}] Last message content ID:`, lastMessageContent.id || 'no-id');
- const markdownDiv = lastMessageContent.querySelector('.markdown');
- if (markdownDiv) {
- console.log(`[${this.name}] Found markdown div`);
- const paragraphs = markdownDiv.querySelectorAll('p');
- if (paragraphs && paragraphs.length > 0) {
- console.log(`[${this.name}] Found ${paragraphs.length} paragraphs in markdown div`);
- let combinedText = "";
- paragraphs.forEach((p, index) => {
- const text = p.textContent.trim();
- console.log(`[${this.name}] Paragraph ${index} text:`, text.substring(0, 30) + (text.length > 30 ? "..." : ""));
- if (text &&
- text !== this.lastSentMessage &&
- !text.includes("Loading") &&
- !text.includes("Thinking") &&
- !text.includes("You stopped this response")) {
- combinedText += text + "\n";
- }
+ // Fallback: if textContent was empty but there are tags inside the matched element.
+ if (!foundResponse && responseText === "" && element.querySelectorAll) {
+ const paragraphs = element.querySelectorAll('p');
+ if (paragraphs && paragraphs.length > 0) {
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] textContent was empty, trying to combine ${paragraphs.length} tags.`);
+ let combinedPText = "";
+ paragraphs.forEach(p => {
+ combinedPText += p.textContent.trim() + "\n";
});
- if (combinedText.trim()) {
- responseText = combinedText.trim();
- console.log(`[${this.name}] Found response in paragraphs:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
- foundResponse = true;
+ responseText = combinedPText.trim();
+ // Re-validate
+ if (responseText &&
+ responseText !== this.lastSentMessage &&
+ !responseText.toLowerCase().includes("loading") &&
+ !responseText.toLowerCase().includes("thinking") &&
+ !responseText.includes("You stopped this response")) {
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Valid response found from combined tags:`, responseText.substring(0, 100) + (responseText.length > 100 ? "..." : ""));
+ foundResponse = true;
} else {
- console.log(`[${this.name}] No valid text found in paragraphs`);
+ responseText = ""; // Clear if not valid after combining paragraphs
}
- } else {
- console.log(`[${this.name}] No paragraphs found in markdown div`);
- const text = markdownDiv.textContent.trim();
- if (text &&
- text !== this.lastSentMessage &&
- !text.includes("Loading") &&
- !text.includes("Thinking") &&
- !text.includes("You stopped this response")) {
- responseText = text;
- console.log(`[${this.name}] Found response in markdown div:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
- foundResponse = true;
- } else {
- console.log(`[${this.name}] Markdown div text appears to be invalid:`, text.substring(0, 50) + (text.length > 50 ? "..." : ""));
- }
- }
- } else {
- console.log(`[${this.name}] No markdown div found in message-content`);
- const text = lastMessageContent.textContent.trim();
- if (text &&
- text !== this.lastSentMessage &&
- !text.includes("Loading") &&
- !text.includes("Thinking") &&
- !text.includes("You stopped this response")) {
- responseText = text;
- console.log(`[${this.name}] Found response in message-content:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
- foundResponse = true;
- } else {
- console.log(`[${this.name}] Message-content text appears to be invalid:`, text.substring(0, 50) + (text.length > 50 ? "..." : ""));
- }
}
- } else {
- console.log(`[${this.name}] No message-content elements found`);
- }
}
- if (!foundResponse) {
- console.log(`[${this.name}] Trying to find paragraphs in the document...`);
- const paragraphs = document.querySelectorAll('p');
- if (paragraphs && paragraphs.length > 0) {
- console.log(`[${this.name}] Found ${paragraphs.length} paragraphs`);
- let combinedText = "";
- for (let i = paragraphs.length - 1; i >= 0; i--) {
- const paragraph = paragraphs[i];
- const text = paragraph.textContent.trim();
- const isUserQuery = paragraph.closest('.user-query-container, .user-query-bubble-container');
- if (isUserQuery) {
- continue;
- }
- if (text &&
- text !== this.lastSentMessage &&
- !text.includes("Loading") &&
- !text.includes("Thinking") &&
- !text.includes("You stopped this response")) {
- combinedText = text + "\n" + combinedText;
- if (text.startsWith("Hello") || text.includes("I'm doing") || text.includes("How can I assist")) {
- break;
- }
- }
- }
- if (combinedText.trim()) {
- responseText = combinedText.trim();
- console.log(`[${this.name}] Found response in paragraphs:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
- foundResponse = true;
- } else {
- console.log(`[${this.name}] No valid text found in paragraphs`);
- }
- } else {
- console.log(`[${this.name}] No paragraphs found`);
- }
- }
-
- if (!foundResponse) {
- console.log(`[${this.name}] Response not found yet, will try again in the next polling cycle`);
- }
} catch (error) {
- console.error(`[${this.name}] Error capturing response from Gemini:`, error);
+ console.error(`[${this.name}] [${this.captureMethod.toUpperCase()}] Error capturing response from Gemini:`, error);
}
+ // Final cleanup (mostly for newlines)
if (foundResponse && responseText) {
- console.log(`[${this.name}] Cleaning up response text...`);
- responseText = responseText.trim()
- .replace(/^(Loading|Thinking).*/gim, '')
- .replace(/You stopped this response.*/gim, '')
- .replace(/\n{3,}/g, '\n\n')
- .trim();
- console.log(`[${this.name}] Cleaned response text:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
+ responseText = responseText.replace(/\n{3,}/g, '\n\n').trim();
}
-
+
return {
found: foundResponse && !!responseText.trim(),
text: responseText
};
}
- // (NEW) Method for streaming API patterns
- getStreamingApiPatterns() {
- console.log(`[${this.name}] getStreamingApiPatterns called`);
- // TODO: DEVELOPER ACTION REQUIRED!
- // Use browser Network DevTools on gemini.google.com to identify the
- // exact URL(s) that deliver the AI's streaming response when a prompt is sent.
- // Replace the placeholder pattern below with the correct one(s).
- // Example: return [{ urlPattern: "*://gemini.google.com/api/generate*", requestStage: "Response" }];
- return [
- { urlPattern: "*://gemini.google.com/api/stream/generateContent*", requestStage: "Response" } // Placeholder - VERIFY THIS!
- ];
+ // --- START: Methods for Debugger-based Response Capture ---
+
+ initiateResponseCapture(requestId, responseCallback) {
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] initiateResponseCapture called for requestId: ${requestId}. Current provider captureMethod: ${this.captureMethod}`);
+ this.pendingResponseCallbacks.set(requestId, responseCallback);
+ if (this.captureMethod === "debugger") {
+ // Ensure accumulator is ready for this request
+ if (!this.requestAccumulators.has(requestId)) {
+ this.requestAccumulators.set(requestId, { text: "", isDefinitelyFinal: false });
+ }
+ console.log(`[${this.name}] [DEBUGGER] Debugger capture selected. Callback stored for requestId: ${requestId}. Ensure background script is set up for '${this.debuggerUrlPattern}'.`);
+ } else if (this.captureMethod === "dom") {
+ // For DOM method, no specific initiation is needed here as polling is handled by content.js
+ console.log(`[${this.name}] [DOM] DOM capture selected. Response will be handled by polling in content.js using captureResponse.`);
+ } else {
+ console.error(`[${this.name}] [${this.captureMethod.toUpperCase()}] Unknown capture method: ${this.captureMethod}`);
+ responseCallback(requestId, `[Error: Unknown capture method '${this.captureMethod}' in provider]`, true);
+ this.pendingResponseCallbacks.delete(requestId);
+ }
}
- // (NEW) Optional Fallback Methods
+ handleDebuggerData(requestId, rawData, isFinalFromBackground) {
+ // This method is only relevant if this.captureMethod === "debugger"
+ console.log(`[${this.name}] [DEBUGGER] handleDebuggerData ENTER - requestId: ${requestId}, isFinalFromBackground: ${isFinalFromBackground}, rawData: "${rawData ? rawData.substring(0,100) + "..." : "null/empty"}"`);
+ const callback = this.pendingResponseCallbacks.get(requestId);
+ if (!callback) {
+ console.warn(`[${this.name}] [DEBUGGER] handleDebuggerData - No callback for requestId: ${requestId}.`);
+ return;
+ }
+
+ let accumulator = this.requestAccumulators.get(requestId);
+ if (!accumulator) {
+ console.warn(`[${this.name}] [DEBUGGER] handleDebuggerData - No accumulator for requestId: ${requestId}. Initializing.`);
+ accumulator = { text: "", isDefinitelyFinal: false };
+ this.requestAccumulators.set(requestId, accumulator);
+ }
+
+ if (accumulator.isDefinitelyFinal) {
+ console.log(`[${this.name}] [DEBUGGER] handleDebuggerData - Accumulator for ${requestId} is already final. Skipping.`);
+ return;
+ }
+
+ if (rawData && rawData.trim() !== "") {
+ const parseOutput = this.parseDebuggerResponse(rawData); // Gemini-specific parsing
+ console.log(`[${this.name}] [DEBUGGER] handleDebuggerData - requestId: ${requestId}, parseOutput: ${JSON.stringify(parseOutput)}`);
+
+ if (parseOutput.text !== null) { // Allow empty string if it's a valid part of the response
+ accumulator.text = parseOutput.text; // parseOutput.text is the total text from rawData
+ }
+
+ if (parseOutput.isFinalResponse) { // If the parser itself detected a definitive end
+ accumulator.isDefinitelyFinal = true;
+ console.log(`[${this.name}] [DEBUGGER] handleDebuggerData - ${requestId} marked as definitelyFinal by parseOutput.`);
+ }
+ }
+
+ const isFinalForCallback = accumulator.isDefinitelyFinal || isFinalFromBackground;
+
+ if (accumulator.text || isFinalForCallback) {
+ console.log(`[${this.name}] [DEBUGGER] handleDebuggerData - INVOKING CALLBACK for ${requestId}. Text length: ${accumulator.text.length}, isFinal: ${isFinalForCallback}`);
+ callback(requestId, accumulator.text, isFinalForCallback);
+ }
+
+ if (isFinalForCallback) {
+ console.log(`[${this.name}] [DEBUGGER] handleDebuggerData - CLEANING UP for ${requestId} (isDefinitelyFinal: ${accumulator.isDefinitelyFinal}, isFinalFromBackground: ${isFinalFromBackground}).`);
+ this.pendingResponseCallbacks.delete(requestId);
+ this.requestAccumulators.delete(requestId);
+ }
+ }
+
+ // TODO: DEVELOPER ACTION REQUIRED!
+ // This is a placeholder parser. You MUST inspect Gemini's actual SSE
+ // stream format and update this parser accordingly.
+ parseDebuggerResponse(rawDataString) {
+ let accumulatedTextFromThisCall = "";
+ let isStreamDefinitelyFinished = false;
+
+ console.log(`[${this.name}] [DEBUGGER] parseDebuggerResponse INPUT rawDataString (len ${rawDataString.length}): ${rawDataString.substring(0, 300)}`);
+
+ if (!rawDataString) {
+ console.log(`[${this.name}] [DEBUGGER] parseDebuggerResponse: Empty rawDataString received.`);
+ return { text: "", isFinalResponse: false };
+ }
+
+ let cleanData = rawDataString;
+ if (cleanData.startsWith(")]}'")) {
+ const firstNewlineIndex = cleanData.indexOf('\n');
+ if (firstNewlineIndex !== -1) {
+ cleanData = cleanData.substring(firstNewlineIndex + 1);
+ } else {
+ console.log(`[${this.name}] [DEBUGGER] parseDebuggerResponse: rawDataString starts with )]}' but no newline found.`);
+ cleanData = "";
+ }
+ }
+ cleanData = cleanData.trimStart();
+ console.log(`[${this.name}] [DEBUGGER] parseDebuggerResponse cleanData after prefix strip (len ${cleanData.length}): ${cleanData.substring(0, 300)}`);
+
+ const chunks = [];
+ let currentIndex = 0;
+ while (currentIndex < cleanData.length) {
+ // Skip leading whitespace/newlines to find the start of a potential length line
+ while (currentIndex < cleanData.length && /\s/.test(cleanData.charAt(currentIndex))) {
+ currentIndex++;
+ }
+ if (currentIndex >= cleanData.length) { // Reached end after skipping whitespace
+ // console.log(`[${this.name}] Reached end of data after skipping initial whitespace.`);
+ break;
+ }
+
+ const nextNewline = cleanData.indexOf('\n', currentIndex);
+ if (nextNewline === -1) {
+ // Last line processing (potential trailing length or garbage)
+ const remainingStr = cleanData.substring(currentIndex).trim();
+ if (/^\d+$/.test(remainingStr) && chunks.length > 0) {
+ // console.log(`[${this.name}] [DEBUGGER] Trailing number found, likely length for a future (missing) chunk: ${remainingStr}`);
+ } else if (remainingStr.length > 0 && chunks.length > 0) { // If there's non-numeric trailing data and we have prior chunks, it might be an error or unparseable.
+ // console.warn(`[${this.name}] [DEBUGGER] Trailing non-numeric, non-empty data found: ${remainingStr.substring(0,100)}`);
+ } else if (remainingStr.length > 0 && chunks.length === 0) { // If it's the *only* data and not a number.
+ // console.warn(`[${this.name}] [DEBUGGER] Single line of non-numeric, non-empty data found, cannot parse as chunk: ${remainingStr.substring(0,100)}`);
+ }
+ break; // End of data or unparseable trailing data
+ }
+
+ const lengthLineContent = cleanData.substring(currentIndex, nextNewline).trim();
+ let length = NaN;
+
+ if (/^\d+$/.test(lengthLineContent)) { // Check if the line *only* contains digits
+ length = parseInt(lengthLineContent, 10);
+ }
+
+ // console.log(`[${this.name}] [DEBUGGER] Chunk parsing: Potential lengthLineContent="${lengthLineContent}", parsed length=${length}`);
+
+ if (isNaN(length) || length < 0) { // This will catch non-numeric lines or negative/invalid lengths
+ console.warn(`[${this.name}] [DEBUGGER] Invalid, non-positive, or non-numeric length line: "${lengthLineContent}". CurrentIndex: ${currentIndex}. Skipping to next line.`);
+ currentIndex = nextNewline + 1; // Advance past the problematic line
+ continue; // Restart loop to find next potential length line from the new currentIndex
+ }
+
+ const jsonStringStart = nextNewline + 1;
+ const jsonStringEnd = jsonStringStart + length;
+
+ if (length === 0) {
+ // console.log(`[${this.name}] [DEBUGGER] Encountered 0-length chunk at currentIndex ${currentIndex}. Advancing to after its conceptual position.`);
+ currentIndex = jsonStringEnd; // This is effectively nextNewline + 1, the start of where the empty JSON "was"
+ continue;
+ }
+
+ if (jsonStringEnd > cleanData.length) {
+ console.warn(`[${this.name}] [DEBUGGER] Declared length ${length} (from line "${lengthLineContent}") exceeds available data. cleanData.length: ${cleanData.length}, required end: ${jsonStringEnd}. Discarding this length and attempting to resynchronize.`);
+ currentIndex = nextNewline + 1; // Skip the problematic length line
+ continue; // Try to find the next valid length line
+ }
+
+ const jsonString = cleanData.substring(jsonStringStart, jsonStringEnd);
+ // console.log(`[${this.name}] [DEBUGGER] Extracted jsonString (len ${jsonString.length}, expected ${length}): ${jsonString.substring(0,100)}`);
+
+ try {
+ JSON.parse(jsonString);
+ chunks.push(jsonString);
+ currentIndex = jsonStringEnd;
+ } catch (parseError) {
+ const errorMsg = parseError.message || "";
+ const nonWhitespaceMatch = errorMsg.match(/Unexpected non-whitespace character after JSON at position (\d+)/);
+
+ if (nonWhitespaceMatch && nonWhitespaceMatch[1]) {
+ const actualEndPositionInJsonString = parseInt(nonWhitespaceMatch[1], 10);
+ const validJsonSubstring = jsonString.substring(0, actualEndPositionInJsonString);
+
+ try {
+ JSON.parse(validJsonSubstring);
+ chunks.push(validJsonSubstring);
+ currentIndex = jsonStringStart + actualEndPositionInJsonString;
+ console.warn(`[${this.name}] [DEBUGGER] Corrected oversized JSON chunk. Original length ${length} (from line "${lengthLineContent}") was too long. Used shorter valid part of length ${actualEndPositionInJsonString}. New currentIndex: ${currentIndex}. Original error: ${parseError.message}`);
+ } catch (innerParseError) {
+ console.warn(`[${this.name}] [DEBUGGER] Failed to parse even the corrected shorter JSON substring (original length ${length} from line "${lengthLineContent}", attempted correction to ${actualEndPositionInJsonString}). Inner Error: ${innerParseError.message}. JSON hint for corrected: "${validJsonSubstring.substring(0,100)}...". Skipping original length declaration.`);
+ currentIndex = nextNewline + 1;
+ }
+ } else {
+ console.warn(`[${this.name}] [DEBUGGER] Failed to parse JSON chunk (length ${length} from line "${lengthLineContent}", Error: ${parseError.message}). This suggests the reported length was incorrect or JSON malformed. JSON hint: "${jsonString.substring(0, 100)}...". Skipping this length declaration and attempting to resynchronize.`);
+ currentIndex = nextNewline + 1;
+ }
+ continue;
+ }
+ }
+
+ for (const rawChunkJson of chunks) {
+ try {
+ const chunkJsonToParse = rawChunkJson.trim();
+ if (!chunkJsonToParse) {
+ continue;
+ }
+ const outerArray = JSON.parse(chunkJsonToParse);
+ if (!Array.isArray(outerArray)) continue;
+
+ for (const item of outerArray) {
+ if (Array.isArray(item) && item.length > 0) {
+ if (item[0] === "wrb.fr" && item.length >= 3 && typeof item[2] === 'string') {
+ const nestedJsonString = item[2];
+ try {
+ const trimmedNestedJsonString = nestedJsonString.trim();
+ if (!trimmedNestedJsonString) {
+ continue;
+ }
+ const innerData = JSON.parse(trimmedNestedJsonString);
+ if (Array.isArray(innerData) && innerData.length > 4 && Array.isArray(innerData[4])) {
+ for (const contentBlock of innerData[4]) {
+ if (Array.isArray(contentBlock) && contentBlock.length >= 2 && typeof contentBlock[0] === 'string' && contentBlock[0].startsWith("rc_") && Array.isArray(contentBlock[1])) {
+ const newText = contentBlock[1].join("");
+ if (newText) {
+ accumulatedTextFromThisCall += newText;
+ }
+ }
+ }
+ }
+ } catch (e) {
+ console.warn(`[${this.name}] [DEBUGGER] Failed to parse nested JSON: "${nestedJsonString.substring(0,100)}...". Error: ${e.message}`);
+ }
+ } else if (item[0] === "e" && item.length >= 1) {
+ isStreamDefinitelyFinished = true;
+ console.log(`[${this.name}] [DEBUGGER] End-of-stream marker 'e' detected in chunk: ${JSON.stringify(item)}`);
+ }
+ }
+ }
+ } catch (e) {
+ console.warn(`[${this.name}] [DEBUGGER] Failed to parse chunk JSON: "${chunkJsonToParse.substring(0,100)}...". Error: ${e.message}`);
+ }
+ }
+ console.log(`[${this.name}] [DEBUGGER] parseDebuggerResponse FINAL output - text length: ${accumulatedTextFromThisCall.length}, accumulatedText: "${accumulatedTextFromThisCall.substring(0,200)}", isFinalByParser: ${isStreamDefinitelyFinished}`);
+
+ if (accumulatedTextFromThisCall.includes("\\<") || accumulatedTextFromThisCall.includes("\\>")) {
+ console.log(`[${this.name}] [DEBUGGER] Unescaping escaped tool call brackets in final text.`);
+ accumulatedTextFromThisCall = accumulatedTextFromThisCall.replace(/\\/g, ">");
+ }
+
+ if (accumulatedTextFromThisCall.includes("\\_")) {
+ console.log(`[${this.name}] [DEBUGGER] Unescaping escaped underscores in final text.`);
+ accumulatedTextFromThisCall = accumulatedTextFromThisCall.replace(/\\_/g, "_");
+ }
+
+ return { text: accumulatedTextFromThisCall, isFinalResponse: isStreamDefinitelyFinished };
+ }
+
+ // --- END: Methods for Debugger-based Response Capture ---
+
+ getStreamingApiPatterns() {
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] getStreamingApiPatterns called. Current provider captureMethod: ${this.captureMethod}`);
+ if (this.captureMethod === "debugger" && this.debuggerUrlPattern) {
+ console.log(`[${this.name}] [DEBUGGER] Using debugger URL pattern: ${this.debuggerUrlPattern}`);
+ return [{ urlPattern: this.debuggerUrlPattern, requestStage: "Response" }];
+ }
+ console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] No debugger patterns to return (captureMethod is '${this.captureMethod}' or no pattern set).`);
+ return [];
+ }
+
+ // (NEW) Optional Fallback Methods - these are largely placeholders if debugger is primary
async captureResponseDOMFallback() {
console.log(`[${this.name}] captureResponseDOMFallback called. Implement DOM observation logic here if needed as a fallback.`);
// TODO: Implement or verify existing DOM fallback logic for Gemini if it's to be kept.
@@ -424,10 +529,11 @@ class GeminiProvider {
return null;
}
- // Check if we should skip response monitoring (Original - UNCHANGED)
+ // Check if we should skip response monitoring
shouldSkipResponseMonitoring() {
- // We want to monitor for responses now that we've fixed the response capturing
- return false;
+ // If using debugger, we skip DOM-based monitoring.
+ // console.log(`[${this.name}] shouldSkipResponseMonitoring called. Capture method: ${this.captureMethod}`);
+ return this.captureMethod === "debugger";
}
}
|