Major refactor: Multi-user Chrome MCP extension with remote server architecture

This commit is contained in:
nasir@endelospay.com
2025-08-21 20:09:57 +05:00
parent d97cad1736
commit 5d869f6a7c
125 changed files with 16249 additions and 11906 deletions

View File

@@ -1,38 +1,369 @@
import { initNativeHostListener } from './native-host';
import {
initSemanticSimilarityListener,
initializeSemanticEngineIfCached,
} from './semantic-similarity';
// Native messaging removed - using remote server only
// import { initNativeHostListener } from './native-host';
// Temporarily disable semantic similarity to focus on connection issues
// import {
// initSemanticSimilarityListener,
// initializeSemanticEngineIfCached,
// } from './semantic-similarity';
import { initStorageManagerListener } from './storage-manager';
import { cleanupModelCache } from '@/utils/semantic-similarity-engine';
import { RemoteServerClient } from '@/utils/remote-server-client';
import { DEFAULT_CONNECTION_CONFIG } from '@/common/env-config';
import { handleCallTool } from './tools';
// Global remote server client instance
let remoteServerClient: RemoteServerClient | null = null;
/**
* Background script entry point
* Initializes all background services and listeners
*/
export default defineBackground(() => {
// Initialize core services
initNativeHostListener();
initSemanticSimilarityListener();
// Initialize remote server client first (prioritize over native messaging)
initRemoteServerClient();
// Initialize core services (native messaging removed)
// initNativeHostListener();
// initSemanticSimilarityListener();
initStorageManagerListener();
// Initialize browser event listeners for connection persistence
initBrowserEventListeners();
// Conditionally initialize semantic similarity engine if model cache exists
initializeSemanticEngineIfCached()
.then((initialized) => {
if (initialized) {
console.log('Background: Semantic similarity engine initialized from cache');
} else {
console.log(
'Background: Semantic similarity engine initialization skipped (no cache found)',
);
}
})
.catch((error) => {
console.warn('Background: Failed to conditionally initialize semantic engine:', error);
});
// initializeSemanticEngineIfCached()
// .then((initialized) => {
// if (initialized) {
// console.log('Background: Semantic similarity engine initialized from cache');
// } else {
// console.log(
// 'Background: Semantic similarity engine initialization skipped (no cache found)',
// );
// }
// })
// .catch((error) => {
// console.warn('Background: Failed to conditionally initialize semantic engine:', error);
// });
// Initial cleanup on startup
cleanupModelCache().catch((error) => {
console.warn('Background: Initial cache cleanup failed:', error);
});
});
/**
* Initialize remote server client (without auto-connecting)
*/
function initRemoteServerClient() {
try {
remoteServerClient = new RemoteServerClient({
serverUrl: DEFAULT_CONNECTION_CONFIG.serverUrl,
reconnectInterval: DEFAULT_CONNECTION_CONFIG.reconnectInterval,
maxReconnectAttempts: 50, // Increased for better reliability
});
console.log('Background: Remote server client initialized (not connected)');
console.log('Background: Use popup to manually connect to remote server');
} catch (error) {
console.error('Background: Failed to initialize remote server client:', error);
}
}
/**
* Get the remote server client instance
*/
export function getRemoteServerClient(): RemoteServerClient | null {
return remoteServerClient;
}
/**
* Initialize browser event listeners for connection persistence
*/
function initBrowserEventListeners() {
// Listen for browser startup events
chrome.runtime.onStartup.addListener(() => {
console.log('Background: Browser startup detected. Manual connection required via popup.');
if (remoteServerClient) {
console.log('Background: Remote server client ready for manual connection');
}
});
// Listen for extension installation/update events
chrome.runtime.onInstalled.addListener((details) => {
console.log('Background: Extension installed/updated:', details.reason);
if (details.reason === 'update') {
console.log('Background: Extension updated, manual connection required');
}
});
// Listen for browser suspension/resume events (Chrome specific)
if (chrome.runtime.onSuspend) {
chrome.runtime.onSuspend.addListener(() => {
console.log('Background: Browser suspending, connection state saved');
// Connection state is automatically saved when connected
});
}
if (chrome.runtime.onSuspendCanceled) {
chrome.runtime.onSuspendCanceled.addListener(() => {
console.log('Background: Browser suspend canceled, maintaining connection');
});
}
// Monitor tab events to ensure connection persists across tab operations
chrome.tabs.onActivated.addListener((activeInfo) => {
// Connection should persist regardless of tab switches
if (remoteServerClient && remoteServerClient.isConnected()) {
console.log(`Background: Tab switched to ${activeInfo.tabId}, connection maintained`);
}
});
// Monitor window events
chrome.windows.onFocusChanged.addListener((windowId) => {
// Connection should persist regardless of window focus changes
if (
remoteServerClient &&
remoteServerClient.isConnected() &&
windowId !== chrome.windows.WINDOW_ID_NONE
) {
console.log(`Background: Window focus changed to ${windowId}, connection maintained`);
}
});
console.log('Background: Browser event listeners initialized for connection persistence');
// Start periodic connection health check
startConnectionHealthCheck();
}
/**
* Start periodic connection health check to maintain persistent connections
*/
function startConnectionHealthCheck() {
// Check connection health every 5 minutes (for monitoring only, no auto-reconnection)
setInterval(
() => {
if (remoteServerClient) {
const isConnected = remoteServerClient.isConnected();
console.log(`Background: Connection health check - Connected: ${isConnected}`);
if (!isConnected) {
console.log('Background: Connection lost. Use popup to manually reconnect.');
// No automatic reconnection - user must manually reconnect via popup
}
}
},
5 * 60 * 1000,
); // 5 minutes
console.log(
'Background: Connection health check started (monitoring only, no auto-reconnection)',
);
}
/**
* Handle messages from popup for remote server control
*/
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'getRemoteServerStatus') {
const status = remoteServerClient?.getStatus() || {
connected: false,
connecting: false,
reconnectAttempts: 0,
connectionTime: undefined,
serverUrl: DEFAULT_CONNECTION_CONFIG.serverUrl,
};
sendResponse(status);
return true;
}
if (message.type === 'connectRemoteServer') {
if (!remoteServerClient) {
sendResponse({ success: false, error: 'Remote server client not initialized' });
return true;
}
if (remoteServerClient.isConnected()) {
sendResponse({ success: true, message: 'Already connected' });
return true;
}
console.log('Background: Attempting to connect to remote server...');
remoteServerClient
.connect()
.then(() => {
console.log('Background: Successfully connected to remote server');
sendResponse({ success: true });
})
.catch((error) => {
console.error('Background: Failed to connect to remote server:', error);
sendResponse({ success: false, error: error.message });
});
return true;
}
if (message.type === 'disconnectRemoteServer') {
if (!remoteServerClient) {
sendResponse({ success: false, error: 'Remote server client not initialized' });
return true;
}
console.log('Background: Disconnecting from remote server...');
try {
remoteServerClient.disconnect();
console.log('Background: Successfully disconnected from remote server');
sendResponse({ success: true });
} catch (error) {
console.error('Background: Error during disconnect:', error);
sendResponse({
success: false,
error: error instanceof Error ? error.message : 'Disconnect failed',
});
}
return true;
}
if (message.type === 'restoreRemoteConnection') {
if (!remoteServerClient) {
sendResponse({ success: false, error: 'Remote server client not initialized' });
return true;
}
if (remoteServerClient.isConnected()) {
sendResponse({ success: true, message: 'Already connected' });
return true;
}
console.log('Background: Attempting to restore previous connection...');
remoteServerClient
.restoreConnectionFromState()
.then((restored) => {
if (restored) {
console.log('Background: Successfully restored previous connection');
sendResponse({ success: true });
} else {
console.log('Background: No previous connection to restore');
sendResponse({ success: false, error: 'No previous connection found' });
}
})
.catch((error) => {
console.error('Background: Failed to restore previous connection:', error);
sendResponse({ success: false, error: error.message });
});
return true;
}
if (message.type === 'getCurrentUserId') {
if (!remoteServerClient) {
sendResponse({ success: false, error: 'Remote server client not initialized' });
return true;
}
remoteServerClient
.getCurrentUserId()
.then((userId) => {
sendResponse({ success: true, userId });
})
.catch((error) => {
console.error('Background: Failed to get current user ID:', error);
sendResponse({ success: false, error: error.message });
});
return true;
}
if (message.type === 'callTool') {
handleCallTool({ name: message.toolName, args: message.params })
.then((result) => {
sendResponse(result);
})
.catch((error) => {
sendResponse({ error: error.message });
});
return true;
}
if (message.type === 'injectUserIdHelper') {
injectUserIdHelper(message.tabId)
.then((result) => {
sendResponse(result);
})
.catch((error) => {
sendResponse({ success: false, error: error.message });
});
return true;
}
});
/**
* Inject user ID helper script into a specific tab
*/
async function injectUserIdHelper(tabId?: number): Promise<{ success: boolean; message: string }> {
try {
let targetTabId = tabId;
// If no tab ID provided, use the active tab
if (!targetTabId) {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
throw new Error('No active tab found');
}
targetTabId = tabs[0].id;
}
// Inject the user ID helper script
await chrome.scripting.executeScript({
target: { tabId: targetTabId },
files: ['inject-scripts/user-id-helper.js'],
});
// Get current user ID and inject it
if (remoteServerClient) {
const userId = await remoteServerClient.getCurrentUserId();
if (userId) {
// Inject the user ID into the page
await chrome.scripting.executeScript({
target: { tabId: targetTabId },
func: (userId) => {
// Make user ID available globally
(window as any).chromeExtensionUserId = userId;
// Store in sessionStorage
try {
sessionStorage.setItem('chromeExtensionUserId', userId);
} catch (e) {
// Ignore storage errors
}
// Dispatch event for pages waiting for user ID
window.dispatchEvent(
new CustomEvent('chromeExtensionUserIdReady', {
detail: { userId: userId },
}),
);
console.log('Chrome Extension User ID injected:', userId);
},
args: [userId],
});
return {
success: true,
message: `User ID helper injected into tab ${targetTabId} with user ID: ${userId}`,
};
} else {
return {
success: true,
message: `User ID helper injected into tab ${targetTabId} but no user ID available (not connected)`,
};
}
} else {
return {
success: true,
message: `User ID helper injected into tab ${targetTabId} but remote server client not initialized`,
};
}
} catch (error) {
console.error('Failed to inject user ID helper:', error);
throw error;
}
}

View File

@@ -2,18 +2,66 @@ import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
// Default window dimensions
// Default window dimensions - optimized for automation tools
const DEFAULT_WINDOW_WIDTH = 1280;
const DEFAULT_WINDOW_HEIGHT = 720;
interface NavigateToolParams {
url?: string;
newWindow?: boolean;
backgroundPage?: boolean;
width?: number;
height?: number;
refresh?: boolean;
}
/**
* Helper function to create automation-friendly background windows
* Ensures proper dimensions and timing for web automation tools
*/
async function createAutomationFriendlyBackgroundWindow(
url: string,
width: number,
height: number,
): Promise<chrome.windows.Window | null> {
try {
console.log(`Creating automation-friendly background window: ${width}x${height} for ${url}`);
// Create window with optimal settings for automation
const window = await chrome.windows.create({
url: url,
width: width,
height: height,
focused: false, // Don't steal focus from user
state: chrome.windows.WindowState.NORMAL, // Start in normal state
type: 'normal', // Normal window type for full automation compatibility
// Ensure window is created with proper viewport
left: 0, // Position consistently for automation
top: 0,
});
if (window && window.id !== undefined) {
// Wait for window to be properly established
await new Promise((resolve) => setTimeout(resolve, 1500));
// Verify window still exists and has correct dimensions
const windowInfo = await chrome.windows.get(window.id);
if (windowInfo && windowInfo.width === width && windowInfo.height === height) {
console.log(`Background window ${window.id} established with correct dimensions`);
return window;
} else {
console.warn(`Window ${window.id} dimensions may not be correct`);
return window; // Return anyway, might still work
}
}
return null;
} catch (error) {
console.error('Failed to create automation-friendly background window:', error);
return null;
}
}
/**
* Tool for navigating to URLs in browser tabs or windows
*/
@@ -21,11 +69,26 @@ class NavigateTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.NAVIGATE;
async execute(args: NavigateToolParams): Promise<ToolResult> {
// Check if backgroundPage was explicitly provided, if not, check user settings
let backgroundPage = args.backgroundPage;
if (backgroundPage === undefined) {
try {
const result = await chrome.storage.local.get(['openUrlsInBackground']);
// Default to true for background windows (changed from false to true)
backgroundPage =
result.openUrlsInBackground !== undefined ? result.openUrlsInBackground : true;
console.log(`Using stored background page preference: ${backgroundPage}`);
} catch (error) {
console.warn('Failed to load background page preference, using default (true):', error);
backgroundPage = true; // Default to background windows
}
}
const { newWindow = false, width, height, url, refresh = false } = args;
console.log(
`Attempting to ${refresh ? 'refresh current tab' : `open URL: ${url}`} with options:`,
args,
{ ...args, backgroundPage },
);
try {
@@ -121,7 +184,83 @@ class NavigateTool extends BaseBrowserToolExecutor {
}
}
// 2. If URL is not already open, decide how to open it based on options
// 2. Handle background page option
if (backgroundPage) {
console.log(
'Opening URL in background page using full-size window that will be minimized.',
);
const windowWidth = typeof width === 'number' ? width : DEFAULT_WINDOW_WIDTH;
const windowHeight = typeof height === 'number' ? height : DEFAULT_WINDOW_HEIGHT;
// Create automation-friendly background window
const backgroundWindow = await createAutomationFriendlyBackgroundWindow(
url!,
windowWidth,
windowHeight,
);
if (backgroundWindow && backgroundWindow.id !== undefined) {
console.log(
`Background window created with ID: ${backgroundWindow.id}, dimensions: ${windowWidth}x${windowHeight}`,
);
try {
// Verify window still exists before minimizing
const windowInfo = await chrome.windows.get(backgroundWindow.id);
if (windowInfo) {
console.log(
`Minimizing window ${backgroundWindow.id} while preserving automation accessibility`,
);
// Now minimize the window to keep it in background while maintaining automation accessibility
await chrome.windows.update(backgroundWindow.id, {
state: chrome.windows.WindowState.MINIMIZED,
});
console.log(
`URL opened in background Window ID: ${backgroundWindow.id} (${windowWidth}x${windowHeight} then minimized)`,
);
}
} catch (error) {
console.warn(`Failed to minimize window ${backgroundWindow.id}:`, error);
// Continue anyway as the window was created successfully
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message:
'Opened URL in background page (full-size window then minimized for automation compatibility)',
windowId: backgroundWindow.id,
width: windowWidth,
height: windowHeight,
tabs: backgroundWindow.tabs
? backgroundWindow.tabs.map((tab) => ({
tabId: tab.id,
url: tab.url,
}))
: [],
automationReady: true,
minimized: true,
dimensions: `${windowWidth}x${windowHeight}`,
}),
},
],
isError: false,
};
} else {
console.error('Failed to create automation-friendly background window');
return createErrorResponse(
'Failed to create background window with proper automation compatibility',
);
}
}
// 3. If URL is not already open, decide how to open it based on options
const openInNewWindow = newWindow || typeof width === 'number' || typeof height === 'number';
if (openInNewWindow) {

View File

@@ -0,0 +1,184 @@
import { BaseBrowserToolExecutor } from '../base-browser';
import { createErrorResponse, createSuccessResponse } from '../../../../common/tool-handler';
import { ERROR_MESSAGES } from '../../../../common/constants';
export class EnhancedSearchTool extends BaseBrowserToolExecutor {
async chromeSearchGoogle(args: {
query: string;
openGoogle?: boolean;
extractResults?: boolean;
maxResults?: number;
}) {
const { query, openGoogle = true, extractResults = true, maxResults = 10 } = args;
try {
// Step 1: Navigate to Google if requested
if (openGoogle) {
await this.navigateToGoogle();
await this.sleep(3000); // Wait for page to load
}
// Step 2: Find and fill search box
const searchSuccess = await this.performGoogleSearch(query);
if (!searchSuccess) {
return createErrorResponse(
'Failed to perform Google search - could not find or interact with search box',
);
}
// Step 3: Wait for results to load
await this.sleep(3000);
// Step 4: Extract results if requested
if (extractResults) {
const results = await this.extractSearchResults(maxResults);
return createSuccessResponse({
query,
searchCompleted: true,
resultsExtracted: true,
results,
});
}
return createSuccessResponse({
query,
searchCompleted: true,
resultsExtracted: false,
message: 'Google search completed successfully',
});
} catch (error) {
return createErrorResponse(
`Error performing Google search: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
async chromeSubmitForm(args: {
formSelector?: string;
inputSelector?: string;
submitMethod?: 'enter' | 'button' | 'auto';
}) {
const { formSelector = 'form', inputSelector, submitMethod = 'auto' } = args;
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND);
}
const tabId = tabs[0].id;
// Inject form submission script
await this.injectContentScript(tabId, ['inject-scripts/form-submit-helper.js']);
const result = await this.sendMessageToTab(tabId, {
action: 'submitForm',
formSelector,
inputSelector,
submitMethod,
});
if (result.error) {
return createErrorResponse(result.error);
}
return createSuccessResponse(result);
} catch (error) {
return createErrorResponse(
`Error submitting form: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
private async navigateToGoogle(): Promise<void> {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
throw new Error('No active tab found');
}
await chrome.tabs.update(tabs[0].id, { url: 'https://www.google.com' });
}
private async performGoogleSearch(query: string): Promise<boolean> {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
throw new Error('No active tab found');
}
const tabId = tabs[0].id;
// Enhanced search box selectors
const searchSelectors = [
'#APjFqb', // Main Google search box ID
'textarea[name="q"]', // Google search textarea
'input[name="q"]', // Google search input (fallback)
'[role="combobox"]', // Role-based selector
'.gLFyf', // Google search box class
'textarea[aria-label*="Search"]', // Aria-label based
'[title*="Search"]', // Title attribute
'.gsfi', // Google search field input class
'#lst-ib', // Alternative Google search ID
'input[type="search"]', // Generic search input
'textarea[role="combobox"]', // Textarea with combobox role
];
// Inject search helper script
await this.injectContentScript(tabId, ['inject-scripts/enhanced-search-helper.js']);
for (const selector of searchSelectors) {
try {
const result = await this.sendMessageToTab(tabId, {
action: 'performGoogleSearch',
selector,
query,
});
if (result.success) {
return true;
}
} catch (error) {
console.debug(`Search selector ${selector} failed:`, error);
continue;
}
}
return false;
}
private async extractSearchResults(maxResults: number): Promise<any[]> {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
throw new Error('No active tab found');
}
const tabId = tabs[0].id;
const result = await this.sendMessageToTab(tabId, {
action: 'extractSearchResults',
maxResults,
});
return result.results || [];
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Export tool instances
export const searchGoogleTool = new (class extends EnhancedSearchTool {
name = 'chrome_search_google';
async execute(args: any) {
return await this.chromeSearchGoogle(args);
}
})();
export const submitFormTool = new (class extends EnhancedSearchTool {
name = 'chrome_submit_form';
async execute(args: any) {
return await this.chromeSubmitForm(args);
}
})();

View File

@@ -12,3 +12,4 @@ export { historyTool } from './history';
export { bookmarkSearchTool, bookmarkAddTool, bookmarkDeleteTool } from './bookmark';
export { injectScriptTool, sendCommandToInjectScriptTool } from './inject-script';
export { consoleTool } from './console';
export { searchGoogleTool, submitFormTool } from './enhanced-search';

View File

@@ -134,10 +134,13 @@ class NetworkDebuggerStartTool extends BaseBrowserToolExecutor {
}
NetworkDebuggerStartTool.instance = this;
chrome.debugger.onEvent.addListener(this.handleDebuggerEvent.bind(this));
chrome.debugger.onDetach.addListener(this.handleDebuggerDetach.bind(this));
chrome.tabs.onRemoved.addListener(this.handleTabRemoved.bind(this));
chrome.tabs.onCreated.addListener(this.handleTabCreated.bind(this));
// Only add listeners if chrome APIs are available (not during build)
if (typeof chrome !== 'undefined' && chrome.debugger?.onEvent) {
chrome.debugger.onEvent.addListener(this.handleDebuggerEvent.bind(this));
chrome.debugger.onDetach.addListener(this.handleDebuggerDetach.bind(this));
chrome.tabs.onRemoved.addListener(this.handleTabRemoved.bind(this));
chrome.tabs.onCreated.addListener(this.handleTabCreated.bind(this));
}
}
private handleTabRemoved(tabId: number) {

View File

@@ -3,7 +3,7 @@ import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
const DEFAULT_NETWORK_REQUEST_TIMEOUT = 30000; // For sending a single request via content script
const DEFAULT_NETWORK_REQUEST_TIMEOUT = 60000; // For sending a single request via content script - increased from 30000
interface NetworkRequestToolParams {
url: string; // URL is always required

View File

@@ -17,15 +17,31 @@ export interface ToolCallParam {
* Handle tool execution
*/
export const handleCallTool = async (param: ToolCallParam) => {
console.log('🛠️ [Tool Handler] Executing tool:', {
toolName: param.name,
hasArgs: !!param.args,
availableTools: Array.from(toolsMap.keys()),
args: param.args,
});
const tool = toolsMap.get(param.name);
if (!tool) {
console.error('🛠️ [Tool Handler] Tool not found:', param.name);
return createErrorResponse(`Tool ${param.name} not found`);
}
try {
return await tool.execute(param.args);
console.log('🛠️ [Tool Handler] Starting tool execution for:', param.name);
const result = await tool.execute(param.args);
console.log('🛠️ [Tool Handler] Tool execution completed:', {
toolName: param.name,
hasResult: !!result,
isError: result?.isError,
result,
});
return result;
} catch (error) {
console.error(`Tool execution failed for ${param.name}:`, error);
console.error(`🛠️ [Tool Handler] Tool execution failed for ${param.name}:`, error);
return createErrorResponse(
error instanceof Error ? error.message : ERROR_MESSAGES.TOOL_EXECUTION_FAILED,
);

View File

@@ -1,4 +1,44 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {},
matches: ['<all_urls>'],
main() {
// Content script is now properly configured for all URLs
// The actual functionality is handled by dynamically injected scripts
// This ensures the content script context is available for communication
console.log('Chrome MCP Extension content script loaded');
// Make user ID available globally on any page
setupUserIdAccess();
},
});
async function setupUserIdAccess() {
try {
// Get user ID from background script
const response = await chrome.runtime.sendMessage({ type: 'getCurrentUserId' });
if (response && response.success && response.userId) {
// Make user ID available globally on the page
(window as any).chromeExtensionUserId = response.userId;
// Also store in a custom event for pages that need it
window.dispatchEvent(
new CustomEvent('chromeExtensionUserIdReady', {
detail: { userId: response.userId },
}),
);
// Store in sessionStorage for easy access
try {
sessionStorage.setItem('chromeExtensionUserId', response.userId);
} catch (e) {
// Ignore storage errors (some sites block this)
}
console.log('Chrome Extension User ID available:', response.userId);
} else {
console.log('Chrome Extension: No user ID available (not connected to server)');
}
} catch (error) {
console.error('Chrome Extension: Failed to get user ID:', error);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -244,3 +244,348 @@ select:focus {
transition-duration: 0.01ms !important;
}
}
/* Enhanced Connection Status Display */
.connection-status-display {
margin: var(--spacing-lg) 0;
padding: var(--spacing-lg);
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
.status-indicator {
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
}
.status-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
flex-shrink: 0;
transition: all var(--transition-normal);
}
.status-icon.status-connected {
background: var(--success-color);
color: white;
}
.status-icon.status-connecting {
background: var(--warning-color);
color: white;
}
.status-icon.status-disconnected {
background: var(--text-muted);
color: white;
}
.status-icon.status-error {
background: var(--error-color);
color: white;
}
.status-details {
flex: 1;
min-width: 0;
}
.status-text-primary {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.status-error {
font-size: 14px;
color: var(--error-color);
margin-bottom: var(--spacing-xs);
word-wrap: break-word;
}
.status-info {
font-size: 12px;
color: var(--text-muted);
margin-bottom: var(--spacing-xs);
}
.status-info:last-child {
margin-bottom: 0;
}
/* Loading Spinner */
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Enhanced Connect Button States */
.connect-button--connected {
background: var(--success-color) !important;
}
.connect-button--connected:hover:not(:disabled) {
background: #38a169 !important;
}
.connect-button--connecting {
background: var(--warning-color) !important;
position: relative;
overflow: hidden;
}
.connect-button--connecting::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: shimmer 1.5s infinite;
}
.connect-button--error {
background: var(--error-color) !important;
}
.connect-button--error:hover:not(:disabled) {
background: #e53e3e !important;
}
@keyframes shimmer {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
/* Error Actions and Help */
.error-actions {
margin-top: var(--spacing-sm);
display: flex;
gap: var(--spacing-sm);
}
.retry-button,
.help-button {
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-primary);
color: var(--text-secondary);
font-size: 12px;
cursor: pointer;
transition: all var(--transition-fast);
}
.retry-button:hover:not(:disabled) {
background: var(--info-color);
color: white;
border-color: var(--info-color);
}
.help-button:hover {
background: var(--warning-color);
color: white;
border-color: var(--warning-color);
}
.retry-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.connection-help {
margin-top: var(--spacing-md);
padding: var(--spacing-md);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
animation: slideDown var(--transition-normal);
}
.help-content h4 {
margin: 0 0 var(--spacing-sm) 0;
color: var(--text-primary);
font-size: 14px;
}
.help-content ul {
margin: 0 0 var(--spacing-md) 0;
padding-left: var(--spacing-lg);
color: var(--text-secondary);
font-size: 12px;
line-height: 1.5;
}
.help-content li {
margin-bottom: var(--spacing-xs);
}
.close-help-button {
padding: var(--spacing-xs) var(--spacing-md);
background: var(--primary-color);
color: white;
border: none;
border-radius: var(--radius-sm);
font-size: 12px;
cursor: pointer;
transition: background var(--transition-fast);
}
.close-help-button:hover {
background: var(--primary-dark);
}
/* Connection Settings */
.connection-settings {
margin: var(--spacing-md) 0;
padding: var(--spacing-md);
background: var(--bg-tertiary);
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
}
.setting-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
cursor: pointer;
user-select: none;
}
.setting-checkbox {
width: 16px;
height: 16px;
accent-color: var(--primary-color);
cursor: pointer;
}
.setting-label {
font-size: 14px;
color: var(--text-secondary);
cursor: pointer;
}
/* Advanced Settings */
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border-light);
}
.settings-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.settings-toggle {
padding: var(--spacing-xs);
background: none;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-size: 12px;
cursor: pointer;
transition: all var(--transition-fast);
min-width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.settings-toggle:hover {
background: var(--bg-secondary);
border-color: var(--primary-color);
color: var(--primary-color);
}
.advanced-settings {
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border-light);
animation: slideDown var(--transition-normal);
}
.setting-group {
margin-bottom: var(--spacing-md);
}
.setting-label-block {
display: block;
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.setting-input {
width: 100%;
padding: var(--spacing-sm);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-primary);
color: var(--text-primary);
font-size: 12px;
transition: all var(--transition-fast);
}
.setting-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
.setting-input:invalid {
border-color: var(--error-color);
}
.setting-actions {
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border-light);
}
.reset-button {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--warning-color);
color: white;
border: none;
border-radius: var(--radius-sm);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background var(--transition-fast);
}
.reset-button:hover {
background: #d69e2e;
}