feat: Implement SigSocket request queuing and approval system, Enhance Settings UI
This commit is contained in:
		| @@ -29,7 +29,21 @@ function startSessionTimeout() { | ||||
|       if (vault && currentSession) { | ||||
|         // Lock the session | ||||
|         vault.lock_session(); | ||||
|  | ||||
|         // Keep the session info for SigSocket connection but mark it as timed out | ||||
|         const keyspace = currentSession.keyspace; | ||||
|         await sessionManager.clear(); | ||||
|  | ||||
|         // Maintain SigSocket connection for the locked keyspace to receive pending requests | ||||
|         if (sigSocketService && keyspace) { | ||||
|           try { | ||||
|             // Keep SigSocket connected to receive requests even when locked | ||||
|             console.log(`🔒 Session timed out but maintaining SigSocket connection for: ${keyspace}`); | ||||
|           } catch (error) { | ||||
|             console.warn('Failed to maintain SigSocket connection after timeout:', error); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // Notify popup if it's open | ||||
|         if (popupPort) { | ||||
|           popupPort.postMessage({ | ||||
| @@ -130,12 +144,48 @@ async function restoreSession() { | ||||
|     if (isUnlocked) { | ||||
|       // Restart keep-alive for restored session | ||||
|       startKeepAlive(); | ||||
|  | ||||
|       // Connect to SigSocket for the restored session | ||||
|       if (sigSocketService) { | ||||
|         try { | ||||
|           const connected = await sigSocketService.connectToServer(session.keyspace); | ||||
|           if (connected) { | ||||
|             console.log(`🔗 SigSocket reconnected for restored workspace: ${session.keyspace}`); | ||||
|           } | ||||
|         } catch (error) { | ||||
|           // Don't show as warning if it's just "no workspace" - this is expected on fresh start | ||||
|           if (error.message && error.message.includes('Workspace not found')) { | ||||
|             console.log(`ℹ️ SigSocket connection skipped for restored session: No workspace available yet`); | ||||
|           } else { | ||||
|             console.warn('Failed to reconnect SigSocket for restored session:', error); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       return session; | ||||
|     } else { | ||||
|       await sessionManager.clear(); | ||||
|       // Session exists but is locked - still try to connect SigSocket to receive pending requests | ||||
|       if (sigSocketService && session.keyspace) { | ||||
|         try { | ||||
|           const connected = await sigSocketService.connectToServer(session.keyspace); | ||||
|           if (connected) { | ||||
|             console.log(`🔗 SigSocket connected for locked workspace: ${session.keyspace} (will queue requests)`); | ||||
|           } | ||||
|         } catch (error) { | ||||
|           // Don't show as warning if it's just "no workspace" - this is expected on fresh start | ||||
|           if (error.message && error.message.includes('Workspace not found')) { | ||||
|             console.log(`ℹ️ SigSocket connection skipped for locked session: No workspace available yet`); | ||||
|           } else { | ||||
|             console.warn('Failed to connect SigSocket for locked session:', error); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Don't clear the session - keep it for SigSocket connection | ||||
|       // await sessionManager.clear(); | ||||
|     } | ||||
|   } | ||||
|   return null; | ||||
|   return session; // Return session even if locked, so we know which keyspace to use | ||||
| } | ||||
|  | ||||
| // Import WASM module functions and SigSocket service | ||||
| @@ -187,14 +237,31 @@ const messageHandlers = { | ||||
|     // Smart auto-connect to SigSocket when session is initialized | ||||
|     if (sigSocketService) { | ||||
|       try { | ||||
|         console.log(`🔗 Initializing SigSocket connection for workspace: ${request.keyspace}`); | ||||
|  | ||||
|         // This will reuse existing connection if same workspace, or switch if different | ||||
|         const connected = await sigSocketService.connectToServer(request.keyspace); | ||||
|         if (connected) { | ||||
|           console.log(`🔗 SigSocket ready for workspace: ${request.keyspace}`); | ||||
|           console.log(`✅ SigSocket ready for workspace: ${request.keyspace}`); | ||||
|         } else { | ||||
|           console.warn(`⚠️ SigSocket connection failed for workspace: ${request.keyspace}`); | ||||
|         } | ||||
|       } catch (error) { | ||||
|         console.warn('Failed to auto-connect to SigSocket:', error); | ||||
|  | ||||
|         // If connection fails, try once more after a short delay | ||||
|         setTimeout(async () => { | ||||
|           try { | ||||
|             console.log(`🔄 Retrying SigSocket connection for workspace: ${request.keyspace}`); | ||||
|             await sigSocketService.connectToServer(request.keyspace); | ||||
|           } catch (retryError) { | ||||
|             console.warn('SigSocket retry connection also failed:', retryError); | ||||
|           } | ||||
|         }, 2000); | ||||
|       } | ||||
|  | ||||
|       // Notify SigSocket service that keyspace is now unlocked | ||||
|       await sigSocketService.onKeypaceUnlocked(); | ||||
|     } | ||||
|  | ||||
|     return { success: true }; | ||||
| @@ -288,6 +355,19 @@ const messageHandlers = { | ||||
|     return { success: true }; | ||||
|   }, | ||||
|  | ||||
|   updateSigSocketUrl: async (request) => { | ||||
|     if (sigSocketService) { | ||||
|       // Update the server URL in the SigSocket service | ||||
|       sigSocketService.defaultServerUrl = request.serverUrl; | ||||
|  | ||||
|       // Save to storage (already done in popup, but ensure consistency) | ||||
|       await chrome.storage.local.set({ sigSocketUrl: request.serverUrl }); | ||||
|  | ||||
|       console.log(`🔗 SigSocket server URL updated to: ${request.serverUrl}`); | ||||
|     } | ||||
|     return { success: true }; | ||||
|   }, | ||||
|  | ||||
|   // SigSocket handlers | ||||
|   connectSigSocket: async (request) => { | ||||
|     if (!sigSocketService) { | ||||
| @@ -313,6 +393,15 @@ const messageHandlers = { | ||||
|     return { success: true, status }; | ||||
|   }, | ||||
|  | ||||
|   getSigSocketStatusWithTest: async () => { | ||||
|     if (!sigSocketService) { | ||||
|       return { success: false, error: 'SigSocket service not initialized' }; | ||||
|     } | ||||
|     // Use the enhanced connection testing method | ||||
|     const status = await sigSocketService.getStatusWithConnectionTest(); | ||||
|     return { success: true, status }; | ||||
|   }, | ||||
|  | ||||
|   getPendingSignRequests: async () => { | ||||
|     if (!sigSocketService) { | ||||
|       return { success: false, error: 'SigSocket service not initialized' }; | ||||
| @@ -393,6 +482,25 @@ chrome.runtime.onConnect.addListener((port) => { | ||||
|       startKeepAlive(); | ||||
|     } | ||||
|  | ||||
|     // Handle messages from popup | ||||
|     port.onMessage.addListener(async (message) => { | ||||
|       if (message.type === 'REQUEST_IMMEDIATE_STATUS') { | ||||
|         // Immediately send current SigSocket status to popup | ||||
|         if (sigSocketService) { | ||||
|           try { | ||||
|             const status = await sigSocketService.getStatus(); | ||||
|             port.postMessage({ | ||||
|               type: 'CONNECTION_STATUS_CHANGED', | ||||
|               status: status | ||||
|             }); | ||||
|             console.log('📡 Sent immediate status to popup:', status.isConnected ? 'Connected' : 'Disconnected'); | ||||
|           } catch (error) { | ||||
|             console.warn('Failed to send immediate status:', error); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     port.onDisconnect.addListener(() => { | ||||
|       // Popup closed, clear reference and stop keep-alive | ||||
|       popupPort = null; | ||||
| @@ -404,4 +512,159 @@ chrome.runtime.onConnect.addListener((port) => { | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
| }); | ||||
|  | ||||
| // Handle notification clicks to open extension (notifications are now clickable without buttons) | ||||
| chrome.notifications.onClicked.addListener(async (notificationId) => { | ||||
|   console.log(`🔔 Notification clicked: ${notificationId}`); | ||||
|  | ||||
|   // Check if this is a SigSocket notification | ||||
|   if (notificationId.startsWith('sigsocket-request-')) { | ||||
|     console.log('🔔 SigSocket notification clicked, opening extension...'); | ||||
|     try { | ||||
|       await openExtensionPopup(); | ||||
|       // Clear the notification after successfully opening | ||||
|       chrome.notifications.clear(notificationId); | ||||
|       console.log('✅ Notification cleared after opening extension'); | ||||
|     } catch (error) { | ||||
|       console.error('❌ Failed to handle notification click:', error); | ||||
|     } | ||||
|   } else { | ||||
|     console.log('🔔 Non-SigSocket notification clicked, ignoring'); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| // Note: Notification button handler removed - notifications are now clickable without buttons | ||||
|  | ||||
| // Function to open extension popup with best UX | ||||
| async function openExtensionPopup() { | ||||
|   try { | ||||
|     console.log('🔔 Opening extension popup from notification...'); | ||||
|  | ||||
|     // First, check if there's already a popup window open | ||||
|     const windows = await chrome.windows.getAll({ populate: true }); | ||||
|     const existingPopup = windows.find(window => | ||||
|       window.type === 'popup' && | ||||
|       window.tabs?.some(tab => tab.url?.includes('popup.html')) | ||||
|     ); | ||||
|  | ||||
|     if (existingPopup) { | ||||
|       // Focus existing popup and send focus message | ||||
|       await chrome.windows.update(existingPopup.id, { focused: true }); | ||||
|       console.log('✅ Focused existing popup window'); | ||||
|  | ||||
|       // Send message to focus on SigSocket section | ||||
|       if (popupPort) { | ||||
|         popupPort.postMessage({ | ||||
|           type: 'FOCUS_SIGSOCKET', | ||||
|           fromNotification: true | ||||
|         }); | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Best UX: Try to use the normal popup experience | ||||
|     // The action API gives the same popup as clicking the extension icon | ||||
|     try { | ||||
|       if (chrome.action && chrome.action.openPopup) { | ||||
|         await chrome.action.openPopup(); | ||||
|         console.log('✅ Extension popup opened via action API (best UX - normal popup)'); | ||||
|  | ||||
|         // Send focus message after popup opens | ||||
|         setTimeout(() => { | ||||
|           if (popupPort) { | ||||
|             popupPort.postMessage({ | ||||
|               type: 'FOCUS_SIGSOCKET', | ||||
|               fromNotification: true | ||||
|             }); | ||||
|           } | ||||
|         }, 200); | ||||
|  | ||||
|         return; | ||||
|       } | ||||
|     } catch (actionError) { | ||||
|       // The action API fails when there's no active browser window | ||||
|       // This is common when all browser windows are closed but extension is still running | ||||
|       console.log('⚠️ Action API failed (likely no active window):', actionError.message); | ||||
|  | ||||
|       // Check if we have any normal browser windows | ||||
|       const allWindows = await chrome.windows.getAll(); | ||||
|       const normalWindows = allWindows.filter(w => w.type === 'normal'); | ||||
|  | ||||
|       if (normalWindows.length > 0) { | ||||
|         // We have browser windows, try to focus one and retry action API | ||||
|         try { | ||||
|           const targetWindow = normalWindows.find(w => w.focused) || normalWindows[0]; | ||||
|           await chrome.windows.update(targetWindow.id, { focused: true }); | ||||
|  | ||||
|           // Small delay and retry | ||||
|           await new Promise(resolve => setTimeout(resolve, 100)); | ||||
|           await chrome.action.openPopup(); | ||||
|           console.log('✅ Extension popup opened via action API after focusing window'); | ||||
|  | ||||
|           setTimeout(() => { | ||||
|             if (popupPort) { | ||||
|               popupPort.postMessage({ | ||||
|                 type: 'FOCUS_SIGSOCKET', | ||||
|                 fromNotification: true | ||||
|               }); | ||||
|             } | ||||
|           }, 200); | ||||
|  | ||||
|           return; | ||||
|         } catch (retryError) { | ||||
|           console.log('⚠️ Action API retry also failed:', retryError.message); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // If action API fails completely, we need to create a window | ||||
|     // But let's make it as close to the normal popup experience as possible | ||||
|     console.log('⚠️ Creating popup window as fallback (action API unavailable)'); | ||||
|  | ||||
|     const popupUrl = chrome.runtime.getURL('popup.html?from=notification'); | ||||
|  | ||||
|     // Position the popup where the extension icon would normally show its popup | ||||
|     // Try to position it in the top-right area like a normal extension popup | ||||
|     let left = screen.width - 420; // 400px width + 20px margin | ||||
|     let top = 80; // Below browser toolbar area | ||||
|  | ||||
|     try { | ||||
|       // If we have a browser window, position relative to it | ||||
|       const allWindows = await chrome.windows.getAll(); | ||||
|       const normalWindows = allWindows.filter(w => w.type === 'normal'); | ||||
|  | ||||
|       if (normalWindows.length > 0) { | ||||
|         const referenceWindow = normalWindows[0]; | ||||
|         left = (referenceWindow.left || 0) + (referenceWindow.width || 800) - 420; | ||||
|         top = (referenceWindow.top || 0) + 80; | ||||
|       } | ||||
|     } catch (positionError) { | ||||
|       console.log('⚠️ Could not get window position, using screen-based positioning'); | ||||
|     } | ||||
|  | ||||
|     const newWindow = await chrome.windows.create({ | ||||
|       url: popupUrl, | ||||
|       type: 'popup', | ||||
|       width: 400, | ||||
|       height: 600, | ||||
|       left: Math.max(0, left), | ||||
|       top: Math.max(0, top), | ||||
|       focused: true | ||||
|     }); | ||||
|  | ||||
|     console.log(`✅ Extension popup window created: ${newWindow.id}`); | ||||
|  | ||||
|   } catch (error) { | ||||
|     console.error('❌ Failed to open extension popup:', error); | ||||
|  | ||||
|     // Final fallback: open in new tab (least ideal but still functional) | ||||
|     try { | ||||
|       const popupUrl = chrome.runtime.getURL('popup.html?from=notification'); | ||||
|       await chrome.tabs.create({ url: popupUrl, active: true }); | ||||
|       console.log('✅ Opened extension in new tab as final fallback'); | ||||
|     } catch (tabError) { | ||||
|       console.error('❌ All popup opening methods failed:', tabError); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -25,6 +25,10 @@ class SigSocketService { | ||||
|  | ||||
|         // UI communication | ||||
|         this.popupPort = null; | ||||
|  | ||||
|         // Status monitoring | ||||
|         this.statusMonitorInterval = null; | ||||
|         this.lastKnownConnectionState = false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -44,72 +48,221 @@ class SigSocketService { | ||||
|             console.warn('Failed to load SigSocket URL from storage:', error); | ||||
|         } | ||||
|  | ||||
|         // Restore any persisted pending requests | ||||
|         await this.restorePendingRequests(); | ||||
|  | ||||
|         console.log('🔌 SigSocket service initialized with WASM APIs'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Restore pending requests from persistent storage | ||||
|      * Only restore requests that match the current workspace | ||||
|      */ | ||||
|     async restorePendingRequests() { | ||||
|         try { | ||||
|             const result = await chrome.storage.local.get(['sigSocketPendingRequests']); | ||||
|             if (result.sigSocketPendingRequests && Array.isArray(result.sigSocketPendingRequests)) { | ||||
|                 console.log(`🔄 Found ${result.sigSocketPendingRequests.length} stored requests`); | ||||
|  | ||||
|                 // Filter requests for current workspace only | ||||
|                 const currentWorkspaceRequests = result.sigSocketPendingRequests.filter(request => | ||||
|                     request.target_public_key === this.connectedPublicKey | ||||
|                 ); | ||||
|  | ||||
|                 console.log(`🔄 Restoring ${currentWorkspaceRequests.length} requests for current workspace`); | ||||
|  | ||||
|                 // Add each workspace-specific request back to WASM storage | ||||
|                 for (const request of currentWorkspaceRequests) { | ||||
|                     try { | ||||
|                         await this.wasmModule.SigSocketManager.add_pending_request(JSON.stringify(request.request || request)); | ||||
|                         console.log(`✅ Restored request: ${request.id || request.request?.id}`); | ||||
|                     } catch (error) { | ||||
|                         console.warn(`Failed to restore request ${request.id || request.request?.id}:`, error); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Update badge after restoration | ||||
|                 this.updateBadge(); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.warn('Failed to restore pending requests:', error); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Persist pending requests to storage with workspace isolation | ||||
|      */ | ||||
|     async persistPendingRequests() { | ||||
|         try { | ||||
|             const requests = await this.getFilteredRequests(); | ||||
|  | ||||
|             // Get existing storage to merge with other workspaces | ||||
|             const result = await chrome.storage.local.get(['sigSocketPendingRequests']); | ||||
|             const existingRequests = result.sigSocketPendingRequests || []; | ||||
|  | ||||
|             // Remove old requests for current workspace | ||||
|             const otherWorkspaceRequests = existingRequests.filter(request => | ||||
|                 request.target_public_key !== this.connectedPublicKey | ||||
|             ); | ||||
|  | ||||
|             // Combine with current workspace requests | ||||
|             const allRequests = [...otherWorkspaceRequests, ...requests]; | ||||
|  | ||||
|             await chrome.storage.local.set({ sigSocketPendingRequests: allRequests }); | ||||
|             console.log(`💾 Persisted ${requests.length} requests for current workspace (${allRequests.length} total)`); | ||||
|         } catch (error) { | ||||
|             console.warn('Failed to persist pending requests:', error); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Connect to SigSocket server using WASM APIs | ||||
|      * WASM handles all connection logic (reuse, switching, etc.) | ||||
|      * @param {string} workspaceId - The workspace/keyspace identifier | ||||
|      * @param {number} retryCount - Number of retry attempts (default: 3) | ||||
|      * @returns {Promise<boolean>} - True if connected successfully | ||||
|      */ | ||||
|     async connectToServer(workspaceId) { | ||||
|         try { | ||||
|             if (!this.wasmModule?.SigSocketManager) { | ||||
|                 throw new Error('WASM SigSocketManager not available'); | ||||
|     async connectToServer(workspaceId, retryCount = 3) { | ||||
|         for (let attempt = 1; attempt <= retryCount; attempt++) { | ||||
|             try { | ||||
|                 if (!this.wasmModule?.SigSocketManager) { | ||||
|                     throw new Error('WASM SigSocketManager not available'); | ||||
|                 } | ||||
|  | ||||
|                 console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId} (attempt ${attempt}/${retryCount})`); | ||||
|  | ||||
|                 // Clean workspace switching | ||||
|                 if (this.currentWorkspace && this.currentWorkspace !== workspaceId) { | ||||
|                     console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${workspaceId}`); | ||||
|                     await this.cleanWorkspaceSwitch(workspaceId); | ||||
|                     // Small delay to ensure clean state transition | ||||
|                     await new Promise(resolve => setTimeout(resolve, 300)); | ||||
|                 } | ||||
|  | ||||
|                 // Let WASM handle all connection logic (reuse, switching, etc.) | ||||
|                 const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events( | ||||
|                     workspaceId, | ||||
|                     this.defaultServerUrl, | ||||
|                     (event) => this.handleSigSocketEvent(event) | ||||
|                 ); | ||||
|  | ||||
|                 // Parse connection info | ||||
|                 const info = JSON.parse(connectionInfo); | ||||
|                 this.currentWorkspace = workspaceId; // Use the parameter we passed, not WASM response | ||||
|                 this.connectedPublicKey = info.public_key; | ||||
|                 this.isConnected = info.is_connected; | ||||
|  | ||||
|                 console.log(`✅ SigSocket connection result:`, { | ||||
|                     workspace: this.currentWorkspace, | ||||
|                     publicKey: this.connectedPublicKey?.substring(0, 16) + '...', | ||||
|                     connected: this.isConnected, | ||||
|                     serverUrl: this.defaultServerUrl | ||||
|                 }); | ||||
|  | ||||
|                 // Validate that we have a public key if connected | ||||
|                 if (this.isConnected && !this.connectedPublicKey) { | ||||
|                     console.warn('⚠️ Connected but no public key received - this may cause request issues'); | ||||
|                 } | ||||
|  | ||||
|                 // Update badge to show current state | ||||
|                 this.updateBadge(); | ||||
|  | ||||
|                 if (this.isConnected) { | ||||
|                     // Clean flow: Connect -> Restore workspace requests -> Update UI | ||||
|                     console.log(`🔗 Connected to workspace: ${workspaceId}, restoring pending requests...`); | ||||
|  | ||||
|                     // 1. Restore requests for this specific workspace | ||||
|                     await this.restorePendingRequests(); | ||||
|  | ||||
|                     // 2. Update badge with current count | ||||
|                     this.updateBadge(); | ||||
|  | ||||
|                     console.log(`✅ Workspace ${workspaceId} ready with restored requests`); | ||||
|                     return true; | ||||
|                 } | ||||
|  | ||||
|                 // If not connected but no error, try again | ||||
|                 if (attempt < retryCount) { | ||||
|                     console.log(`⏳ Connection not established, retrying in 1 second...`); | ||||
|                     await new Promise(resolve => setTimeout(resolve, 1000)); | ||||
|                 } | ||||
|  | ||||
|             } catch (error) { | ||||
|                 // Check if this is an expected "no workspace" error during startup | ||||
|                 const isExpectedStartupError = error.message && | ||||
|                     (error.message.includes('Workspace not found') || | ||||
|                      error.message.includes('no keypairs available')); | ||||
|  | ||||
|                 if (isExpectedStartupError && attempt === 1) { | ||||
|                     console.log(`⏳ SigSocket connection attempt ${attempt}: No active workspace (expected after extension reload)`); | ||||
|                 } | ||||
|  | ||||
|                 // Check if this is a public key related error | ||||
|                 if (error.message && error.message.includes('public key')) { | ||||
|                     console.error(`🔑 Public key error detected: ${error.message}`); | ||||
|                     // For public key errors, don't retry immediately - might need workspace change | ||||
|                     if (attempt === 1) { | ||||
|                         console.log(`🔄 Public key error on first attempt, trying to disconnect and reconnect...`); | ||||
|                         await this.disconnect(); | ||||
|                         await new Promise(resolve => setTimeout(resolve, 1000)); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (attempt < retryCount) { | ||||
|                     if (!isExpectedStartupError) { | ||||
|                         console.log(`⏳ Retrying connection in 2 seconds...`); | ||||
|                     } | ||||
|                     await new Promise(resolve => setTimeout(resolve, 2000)); | ||||
|                 } else { | ||||
|                     // Final attempt failed | ||||
|                     this.isConnected = false; | ||||
|                     this.currentWorkspace = null; | ||||
|                     this.connectedPublicKey = null; | ||||
|  | ||||
|                     if (isExpectedStartupError) { | ||||
|                         console.log(`ℹ️ SigSocket connection failed: No active workspace. Will connect when user logs in.`); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId}`); | ||||
|  | ||||
|             // Let WASM handle all connection logic (reuse, switching, etc.) | ||||
|             const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events( | ||||
|                 workspaceId, | ||||
|                 this.defaultServerUrl, | ||||
|                 (event) => this.handleSigSocketEvent(event) | ||||
|             ); | ||||
|  | ||||
|             // Parse connection info | ||||
|             const info = JSON.parse(connectionInfo); | ||||
|             this.currentWorkspace = info.workspace; | ||||
|             this.connectedPublicKey = info.public_key; | ||||
|             this.isConnected = info.is_connected; | ||||
|  | ||||
|             console.log(`✅ SigSocket connection result:`, { | ||||
|                 workspace: this.currentWorkspace, | ||||
|                 publicKey: this.connectedPublicKey?.substring(0, 16) + '...', | ||||
|                 connected: this.isConnected | ||||
|             }); | ||||
|  | ||||
|             // Update badge to show current state | ||||
|             this.updateBadge(); | ||||
|  | ||||
|             return this.isConnected; | ||||
|  | ||||
|         } catch (error) { | ||||
|             console.error('❌ SigSocket connection failed:', error); | ||||
|             this.isConnected = false; | ||||
|             this.currentWorkspace = null; | ||||
|             this.connectedPublicKey = null; | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Handle events from the WASM SigSocket client | ||||
|      * This is called automatically when requests arrive | ||||
|      * @param {Object} event - Event from WASM layer | ||||
|      */ | ||||
|     handleSigSocketEvent(event) { | ||||
|     async handleSigSocketEvent(event) { | ||||
|         console.log('📨 Received SigSocket event:', event); | ||||
|  | ||||
|         if (event.type === 'sign_request') { | ||||
|             console.log(`🔐 New sign request: ${event.request_id}`); | ||||
|             console.log(`🔐 New sign request: ${event.request_id} for workspace: ${this.currentWorkspace}`); | ||||
|  | ||||
|             // The request is automatically stored by WASM | ||||
|             // We just handle UI updates | ||||
|             this.showSignRequestNotification(); | ||||
|             this.updateBadge(); | ||||
|             this.notifyPopupOfNewRequest(); | ||||
|             // Clean flow: Request arrives -> Store -> Persist -> Update UI | ||||
|             try { | ||||
|                 // 1. Request is automatically stored in WASM (already done by WASM layer) | ||||
|  | ||||
|                 // 2. Persist to storage with workspace isolation | ||||
|                 await this.persistPendingRequests(); | ||||
|  | ||||
|                 // 3. Update badge count | ||||
|                 this.updateBadge(); | ||||
|  | ||||
|                 // 4. Show notification | ||||
|                 this.showSignRequestNotification(); | ||||
|  | ||||
|                 // 5. Notify popup if connected | ||||
|                 this.notifyPopupOfNewRequest(); | ||||
|  | ||||
|                 console.log(`✅ Request ${event.request_id} processed and stored for workspace: ${this.currentWorkspace}`); | ||||
|  | ||||
|             } catch (error) { | ||||
|                 console.error(`❌ Failed to process request ${event.request_id}:`, error); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -124,6 +277,19 @@ class SigSocketService { | ||||
|                 throw new Error('WASM SigSocketManager not available'); | ||||
|             } | ||||
|  | ||||
|             // Check if we're connected before attempting approval | ||||
|             if (!this.isConnected) { | ||||
|                 console.warn(`⚠️ Not connected to SigSocket server, cannot approve request: ${requestId}`); | ||||
|                 throw new Error('Not connected to SigSocket server'); | ||||
|             } | ||||
|  | ||||
|             // Verify we can approve this request | ||||
|             const canApprove = await this.canApproveRequest(requestId); | ||||
|             if (!canApprove) { | ||||
|                 console.warn(`⚠️ Cannot approve request ${requestId} - keyspace may be locked or request not found`); | ||||
|                 throw new Error('Cannot approve request - keyspace may be locked or request not found'); | ||||
|             } | ||||
|  | ||||
|             console.log(`✅ Approving request: ${requestId}`); | ||||
|  | ||||
|             // WASM handles all validation, signing, and server communication | ||||
| @@ -131,14 +297,37 @@ class SigSocketService { | ||||
|  | ||||
|             console.log(`🎉 Request approved successfully: ${requestId}`); | ||||
|  | ||||
|             // Update UI | ||||
|             // Clean flow: Approve -> Remove from storage -> Update UI | ||||
|             // 1. Remove from persistent storage (WASM already removed it) | ||||
|             await this.persistPendingRequests(); | ||||
|  | ||||
|             // 2. Update badge count | ||||
|             this.updateBadge(); | ||||
|  | ||||
|             // 3. Notify popup of updated state | ||||
|             this.notifyPopupOfRequestUpdate(); | ||||
|  | ||||
|             console.log(`✅ Request ${requestId} approved and cleaned up`); | ||||
|             return true; | ||||
|  | ||||
|         } catch (error) { | ||||
|             console.error(`❌ Failed to approve request ${requestId}:`, error); | ||||
|  | ||||
|             // Check if this is a connection-related error | ||||
|             if (error.message && (error.message.includes('Connection not found') || error.message.includes('public key'))) { | ||||
|                 console.error(`🔑 Connection/public key error during approval. Current state:`, { | ||||
|                     connected: this.isConnected, | ||||
|                     workspace: this.currentWorkspace, | ||||
|                     publicKey: this.connectedPublicKey?.substring(0, 16) + '...' | ||||
|                 }); | ||||
|  | ||||
|                 // Try to reconnect for next time | ||||
|                 if (this.currentWorkspace) { | ||||
|                     console.log(`🔄 Attempting to reconnect to workspace: ${this.currentWorkspace}`); | ||||
|                     setTimeout(() => this.connectToServer(this.currentWorkspace), 1000); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| @@ -162,10 +351,17 @@ class SigSocketService { | ||||
|  | ||||
|             console.log(`✅ Request rejected successfully: ${requestId}`); | ||||
|  | ||||
|             // Update UI | ||||
|             // Clean flow: Reject -> Remove from storage -> Update UI | ||||
|             // 1. Remove from persistent storage (WASM already removed it) | ||||
|             await this.persistPendingRequests(); | ||||
|  | ||||
|             // 2. Update badge count | ||||
|             this.updateBadge(); | ||||
|  | ||||
|             // 3. Notify popup of updated state | ||||
|             this.notifyPopupOfRequestUpdate(); | ||||
|  | ||||
|             console.log(`✅ Request ${requestId} rejected and cleaned up`); | ||||
|             return true; | ||||
|  | ||||
|         } catch (error) { | ||||
| @@ -224,18 +420,54 @@ class SigSocketService { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Show notification for new sign request | ||||
|      * Show clickable notification for new sign request | ||||
|      * Call this AFTER the request has been stored and persisted | ||||
|      */ | ||||
|     showSignRequestNotification() { | ||||
|     async showSignRequestNotification() { | ||||
|         try { | ||||
|             if (chrome.notifications && chrome.notifications.create) { | ||||
|                 chrome.notifications.create({ | ||||
|                 // Small delay to ensure request is fully stored | ||||
|                 await new Promise(resolve => setTimeout(resolve, 100)); | ||||
|  | ||||
|                 console.log(`📢 Preparing notification for new signature request`); | ||||
|  | ||||
|                 // Check if keyspace is currently unlocked to customize message | ||||
|                 let message = 'New signature request received. Click to review and approve.'; | ||||
|                 let title = 'SigSocket Sign Request'; | ||||
|  | ||||
|                 // Try to determine if keyspace is locked | ||||
|                 try { | ||||
|                     const requests = await this.getPendingRequests(); | ||||
|                     const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false; | ||||
|                     if (!canApprove) { | ||||
|                         message = 'New signature request received. Click to unlock keyspace and approve.'; | ||||
|                         title = 'SigSocket Request'; | ||||
|                     } | ||||
|                 } catch (error) { | ||||
|                     // If we can't check, use generic message | ||||
|                     message = 'New signature request received. Click to open extension.'; | ||||
|                 } | ||||
|  | ||||
|                 // Create clickable notification with unique ID | ||||
|                 const notificationId = `sigsocket-request-${Date.now()}`; | ||||
|  | ||||
|                 const notificationOptions = { | ||||
|                     type: 'basic', | ||||
|                     iconUrl: 'icons/icon48.png', | ||||
|                     title: 'SigSocket Sign Request', | ||||
|                     message: 'New signature request received. Click to review.' | ||||
|                     title: title, | ||||
|                     message: message, | ||||
|                     requireInteraction: true // Keep notification visible until user interacts | ||||
|                 }; | ||||
|  | ||||
|                 console.log(`📢 Creating notification: ${notificationId}`, notificationOptions); | ||||
|  | ||||
|                 chrome.notifications.create(notificationId, notificationOptions, (createdId) => { | ||||
|                     if (chrome.runtime.lastError) { | ||||
|                         console.error('❌ Failed to create notification:', chrome.runtime.lastError); | ||||
|                     } else { | ||||
|                         console.log(`✅ Notification created successfully: ${createdId}`); | ||||
|                     } | ||||
|                 }); | ||||
|                 console.log('📢 Notification shown for sign request'); | ||||
|             } else { | ||||
|                 console.log('📢 Notifications not available, skipping notification'); | ||||
|             } | ||||
| @@ -322,6 +554,10 @@ class SigSocketService { | ||||
|             this.isConnected = false; | ||||
|             this.currentWorkspace = null; | ||||
|             this.connectedPublicKey = null; | ||||
|             this.lastKnownConnectionState = false; | ||||
|  | ||||
|             // Stop status monitoring | ||||
|             this.stopStatusMonitoring(); | ||||
|  | ||||
|             this.updateBadge(); | ||||
|  | ||||
| @@ -333,7 +569,50 @@ class SigSocketService { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get connection status from WASM | ||||
|      * Clear persisted pending requests from storage | ||||
|      */ | ||||
|     async clearPersistedRequests() { | ||||
|         try { | ||||
|             await chrome.storage.local.remove(['sigSocketPendingRequests']); | ||||
|             console.log('🗑️ Cleared persisted pending requests from storage'); | ||||
|         } catch (error) { | ||||
|             console.warn('Failed to clear persisted requests:', error); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Clean workspace switch - clear current workspace requests only | ||||
|      */ | ||||
|     async cleanWorkspaceSwitch(newWorkspace) { | ||||
|         try { | ||||
|             console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${newWorkspace}`); | ||||
|  | ||||
|             // 1. Persist current workspace requests before switching | ||||
|             if (this.currentWorkspace && this.isConnected) { | ||||
|                 await this.persistPendingRequests(); | ||||
|                 console.log(`💾 Saved requests for workspace: ${this.currentWorkspace}`); | ||||
|             } | ||||
|  | ||||
|             // 2. Clear WASM state (will be restored for new workspace) | ||||
|             if (this.wasmModule?.SigSocketManager) { | ||||
|                 await this.wasmModule.SigSocketManager.clear_pending_requests(); | ||||
|                 console.log('🧹 Cleared WASM request state'); | ||||
|             } | ||||
|  | ||||
|             // 3. Reset local state | ||||
|             this.currentWorkspace = null; | ||||
|             this.connectedPublicKey = null; | ||||
|             this.isConnected = false; | ||||
|  | ||||
|             console.log('✅ Workspace switch cleanup completed'); | ||||
|  | ||||
|         } catch (error) { | ||||
|             console.error('❌ Failed to clean workspace switch:', error); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get connection status with real connection verification | ||||
|      * @returns {Promise<Object>} - Connection status information | ||||
|      */ | ||||
|     async getStatus() { | ||||
| @@ -348,21 +627,63 @@ class SigSocketService { | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             // Let WASM provide the authoritative status | ||||
|             // Get WASM status first | ||||
|             const statusJson = await this.wasmModule.SigSocketManager.get_connection_status(); | ||||
|             const status = JSON.parse(statusJson); | ||||
|             const requests = await this.getPendingRequests(); | ||||
|  | ||||
|             return { | ||||
|                 isConnected: status.is_connected, | ||||
|                 workspace: status.workspace, | ||||
|                 publicKey: status.public_key, | ||||
|             // Verify connection by trying to get requests (this will fail if not connected) | ||||
|             let actuallyConnected = false; | ||||
|             let requests = []; | ||||
|  | ||||
|             try { | ||||
|                 requests = await this.getPendingRequests(); | ||||
|                 // If we can get requests and WASM says connected, we're probably connected | ||||
|                 actuallyConnected = status.is_connected && Array.isArray(requests); | ||||
|             } catch (error) { | ||||
|                 // If getting requests fails, we're definitely not connected | ||||
|                 console.warn('Connection verification failed:', error); | ||||
|                 actuallyConnected = false; | ||||
|             } | ||||
|  | ||||
|             // Update our internal state | ||||
|             this.isConnected = actuallyConnected; | ||||
|  | ||||
|             if (status.connected_public_key && actuallyConnected) { | ||||
|                 this.connectedPublicKey = status.connected_public_key; | ||||
|             } else { | ||||
|                 this.connectedPublicKey = null; | ||||
|             } | ||||
|  | ||||
|             // If we're disconnected, clear our workspace | ||||
|             if (!actuallyConnected) { | ||||
|                 this.currentWorkspace = null; | ||||
|             } | ||||
|  | ||||
|             const statusResult = { | ||||
|                 isConnected: actuallyConnected, | ||||
|                 workspace: this.currentWorkspace, | ||||
|                 publicKey: status.connected_public_key, | ||||
|                 pendingRequestCount: requests.length, | ||||
|                 serverUrl: this.defaultServerUrl | ||||
|                 serverUrl: this.defaultServerUrl, | ||||
|                 // Clean flow status indicators | ||||
|                 cleanFlowReady: actuallyConnected && this.currentWorkspace && status.connected_public_key | ||||
|             }; | ||||
|  | ||||
|             console.log('📊 Clean flow status:', { | ||||
|                 connected: statusResult.isConnected, | ||||
|                 workspace: statusResult.workspace, | ||||
|                 requestCount: statusResult.pendingRequestCount, | ||||
|                 flowReady: statusResult.cleanFlowReady | ||||
|             }); | ||||
|  | ||||
|             return statusResult; | ||||
|  | ||||
|         } catch (error) { | ||||
|             console.error('Failed to get status:', error); | ||||
|             // Clear state on error | ||||
|             this.isConnected = false; | ||||
|             this.currentWorkspace = null; | ||||
|             this.connectedPublicKey = null; | ||||
|             return { | ||||
|                 isConnected: false, | ||||
|                 workspace: null, | ||||
| @@ -375,33 +696,178 @@ class SigSocketService { | ||||
|  | ||||
|     /** | ||||
|      * Set the popup port for communication | ||||
|      * @param {chrome.runtime.Port} port - The popup port | ||||
|      * @param {chrome.runtime.Port|null} port - The popup port or null to disconnect | ||||
|      */ | ||||
|     setPopupPort(port) { | ||||
|         this.popupPort = port; | ||||
|         console.log('📱 Popup connected to SigSocket service'); | ||||
|  | ||||
|         if (port) { | ||||
|             console.log('📱 Popup connected to SigSocket service'); | ||||
|  | ||||
|             // Immediately check connection status when popup opens | ||||
|             this.checkConnectionStatusNow(); | ||||
|  | ||||
|             // Start monitoring connection status when popup connects | ||||
|             this.startStatusMonitoring(); | ||||
|         } else { | ||||
|             console.log('📱 Popup disconnected from SigSocket service'); | ||||
|             // Stop monitoring when popup disconnects | ||||
|             this.stopStatusMonitoring(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when keyspace is unlocked - notify popup of current state | ||||
|      * Immediately check and update connection status | ||||
|      */ | ||||
|     async checkConnectionStatusNow() { | ||||
|         try { | ||||
|             // Force a fresh connection check | ||||
|             const currentStatus = await this.getStatusWithConnectionTest(); | ||||
|             this.lastKnownConnectionState = currentStatus.isConnected; | ||||
|  | ||||
|             // Notify popup of current status | ||||
|             this.notifyPopupOfStatusChange(currentStatus); | ||||
|  | ||||
|             console.log(`🔍 Immediate status check: ${currentStatus.isConnected ? 'Connected' : 'Disconnected'}`); | ||||
|         } catch (error) { | ||||
|             console.warn('Failed to check connection status immediately:', error); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get status with additional connection testing | ||||
|      */ | ||||
|     async getStatusWithConnectionTest() { | ||||
|         const status = await this.getStatus(); | ||||
|  | ||||
|         // If WASM claims we're connected, do an additional verification | ||||
|         if (status.isConnected) { | ||||
|             try { | ||||
|                 // Try to get connection status again - if this fails, we're not really connected | ||||
|                 const verifyJson = await this.wasmModule.SigSocketManager.get_connection_status(); | ||||
|                 const verifyStatus = JSON.parse(verifyJson); | ||||
|  | ||||
|                 if (!verifyStatus.is_connected) { | ||||
|                     console.log('🔍 Connection verification failed - marking as disconnected'); | ||||
|                     status.isConnected = false; | ||||
|                     this.isConnected = false; | ||||
|                     this.currentWorkspace = null; | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 console.log('🔍 Connection test failed - marking as disconnected:', error.message); | ||||
|                 status.isConnected = false; | ||||
|                 this.isConnected = false; | ||||
|                 this.currentWorkspace = null; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return status; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Start periodic status monitoring to detect connection changes | ||||
|      */ | ||||
|     startStatusMonitoring() { | ||||
|         // Clear any existing monitoring | ||||
|         if (this.statusMonitorInterval) { | ||||
|             clearInterval(this.statusMonitorInterval); | ||||
|         } | ||||
|  | ||||
|         // Check status every 2 seconds when popup is open (more responsive) | ||||
|         this.statusMonitorInterval = setInterval(async () => { | ||||
|             if (this.popupPort) { | ||||
|                 try { | ||||
|                     const currentStatus = await this.getStatusWithConnectionTest(); | ||||
|  | ||||
|                     // Check if connection status changed | ||||
|                     if (currentStatus.isConnected !== this.lastKnownConnectionState) { | ||||
|                         console.log(`🔄 Connection state changed: ${this.lastKnownConnectionState} -> ${currentStatus.isConnected}`); | ||||
|                         this.lastKnownConnectionState = currentStatus.isConnected; | ||||
|  | ||||
|                         // Notify popup of status change | ||||
|                         this.notifyPopupOfStatusChange(currentStatus); | ||||
|                     } | ||||
|                 } catch (error) { | ||||
|                     console.warn('Status monitoring error:', error); | ||||
|                     // On error, assume disconnected | ||||
|                     if (this.lastKnownConnectionState !== false) { | ||||
|                         console.log('🔄 Status monitoring error - marking as disconnected'); | ||||
|                         this.lastKnownConnectionState = false; | ||||
|                         this.notifyPopupOfStatusChange({ | ||||
|                             isConnected: false, | ||||
|                             workspace: null, | ||||
|                             publicKey: null, | ||||
|                             pendingRequestCount: 0, | ||||
|                             serverUrl: this.defaultServerUrl | ||||
|                         }); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 // Stop monitoring when popup is closed | ||||
|                 this.stopStatusMonitoring(); | ||||
|             } | ||||
|         }, 2000); // 2 seconds for better responsiveness | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Stop status monitoring | ||||
|      */ | ||||
|     stopStatusMonitoring() { | ||||
|         if (this.statusMonitorInterval) { | ||||
|             clearInterval(this.statusMonitorInterval); | ||||
|             this.statusMonitorInterval = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Notify popup of connection status change | ||||
|      * @param {Object} status - Current connection status | ||||
|      */ | ||||
|     notifyPopupOfStatusChange(status) { | ||||
|         if (this.popupPort) { | ||||
|             this.popupPort.postMessage({ | ||||
|                 type: 'CONNECTION_STATUS_CHANGED', | ||||
|                 status: status | ||||
|             }); | ||||
|             console.log(`📡 Notified popup of connection status change: ${status.isConnected ? 'Connected' : 'Disconnected'}`); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when keyspace is unlocked - clean approach to show pending requests | ||||
|      */ | ||||
|     async onKeypaceUnlocked() { | ||||
|         if (!this.popupPort) return; | ||||
|  | ||||
|         try { | ||||
|             console.log('🔓 Keyspace unlocked - preparing to show pending requests'); | ||||
|  | ||||
|             // 1. Restore any persisted requests for this workspace | ||||
|             await this.restorePendingRequests(); | ||||
|  | ||||
|             // 2. Get current requests (includes restored + any new ones) | ||||
|             const requests = await this.getPendingRequests(); | ||||
|             const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false; | ||||
|  | ||||
|             this.popupPort.postMessage({ | ||||
|                 type: 'KEYSPACE_UNLOCKED', | ||||
|                 canApprove, | ||||
|                 pendingRequests: requests | ||||
|             }); | ||||
|             // 3. Check if we can approve requests (keyspace should be unlocked now) | ||||
|             const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : true; | ||||
|  | ||||
|             console.log(`🔓 Keyspace unlocked notification sent: ${requests.length} requests, canApprove: ${canApprove}`); | ||||
|             // 4. Update badge with current count | ||||
|             this.updateBadge(); | ||||
|  | ||||
|             // 5. Notify popup if connected | ||||
|             if (this.popupPort) { | ||||
|                 this.popupPort.postMessage({ | ||||
|                     type: 'KEYSPACE_UNLOCKED', | ||||
|                     canApprove, | ||||
|                     pendingRequests: requests | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             console.log(`🔓 Keyspace unlocked: ${requests.length} requests ready, canApprove: ${canApprove}`); | ||||
|  | ||||
|             return requests; | ||||
|  | ||||
|         } catch (error) { | ||||
|             console.error('Failed to handle keyspace unlock:', error); | ||||
|             return []; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,28 +7,17 @@ | ||||
| <body> | ||||
|   <div class="container"> | ||||
|     <header class="header"> | ||||
|       <div class="logo"> | ||||
|       <div class="logo clickable-header" id="headerTitle"> | ||||
|         <div class="logo-icon">🔐</div> | ||||
|         <h1>CryptoVault</h1> | ||||
|       </div> | ||||
|       <div class="header-actions"> | ||||
|         <div class="settings-container"> | ||||
|           <button id="settingsToggle" class="btn-icon-only" title="Settings"> | ||||
|             <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|               <circle cx="12" cy="12" r="3"></circle> | ||||
|               <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> | ||||
|             </svg> | ||||
|           </button> | ||||
|           <div class="settings-dropdown hidden" id="settingsDropdown"> | ||||
|             <div class="settings-item"> | ||||
|               <label for="timeoutInput">Session Timeout</label> | ||||
|               <div class="timeout-input-group"> | ||||
|                 <input type="number" id="timeoutInput" min="3" max="300" value="15"> | ||||
|                 <span>seconds</span> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <button id="settingsBtn" class="btn-icon-only" title="Settings"> | ||||
|           <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|             <circle cx="12" cy="12" r="3"></circle> | ||||
|             <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> | ||||
|           </svg> | ||||
|         </button> | ||||
|         <button id="themeToggle" class="btn-icon-only" title="Switch to dark mode"> | ||||
|           <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|             <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> | ||||
| @@ -84,6 +73,14 @@ | ||||
|         </div> | ||||
|  | ||||
|         <div class="requests-container" id="requestsContainer"> | ||||
|           <div class="loading-requests hidden" id="loadingRequestsMessage"> | ||||
|             <div class="loading-state"> | ||||
|               <div class="loading-spinner"></div> | ||||
|               <p>Loading requests...</p> | ||||
|               <small>Fetching pending signature requests</small> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|           <div class="no-requests" id="noRequestsMessage"> | ||||
|             <div class="empty-state"> | ||||
|               <div class="empty-icon">📝</div> | ||||
| @@ -106,14 +103,6 @@ | ||||
|             </svg> | ||||
|             Refresh | ||||
|           </button> | ||||
|           <button id="sigSocketStatusBtn" class="btn btn-ghost btn-small"> | ||||
|             <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|               <circle cx="12" cy="12" r="10"></circle> | ||||
|               <line x1="12" y1="6" x2="12" y2="12"></line> | ||||
|               <line x1="16" y1="16" x2="12" y2="12"></line> | ||||
|             </svg> | ||||
|             Status | ||||
|           </button> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
| @@ -235,8 +224,41 @@ | ||||
|       </div> | ||||
|     </section> | ||||
|  | ||||
|     <!-- Settings Section --> | ||||
|     <section class="section hidden" id="settingsSection"> | ||||
|       <div class="settings-header"> | ||||
|         <h2>Settings</h2> | ||||
|       </div> | ||||
|  | ||||
|       <!-- Session Settings --> | ||||
|       <div class="card"> | ||||
|         <h3>Session Settings</h3> | ||||
|         <div class="settings-item"> | ||||
|           <label for="timeoutInput">Session Timeout</label> | ||||
|           <div class="timeout-input-group"> | ||||
|             <input type="number" id="timeoutInput" min="3" max="300" value="15"> | ||||
|             <span>seconds</span> | ||||
|           </div> | ||||
|           <small class="settings-help">Automatically lock session after inactivity</small> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <!-- SigSocket Settings --> | ||||
|       <div class="card"> | ||||
|         <h3>SigSocket Settings</h3> | ||||
|         <div class="settings-item"> | ||||
|           <label for="serverUrlInput">Server URL</label> | ||||
|           <div class="server-input-group"> | ||||
|             <input type="text" id="serverUrlInput" placeholder="ws://localhost:8080/ws" value="ws://localhost:8080/ws"> | ||||
|             <button id="saveServerUrlBtn" class="btn btn-small btn-primary">Save</button> | ||||
|           </div> | ||||
|           <small class="settings-help">WebSocket URL for SigSocket server (ws:// or wss://)</small> | ||||
|         </div> | ||||
|  | ||||
|  | ||||
|       </div> | ||||
|  | ||||
|     </section> | ||||
|  | ||||
|   </div> | ||||
|  | ||||
|   | ||||
| @@ -32,6 +32,11 @@ function showToast(message, type = 'info') { | ||||
|  | ||||
| // Enhanced loading states for buttons | ||||
| function setButtonLoading(button, loading = true) { | ||||
|   // Handle null/undefined button gracefully | ||||
|   if (!button) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (loading) { | ||||
|     button.dataset.originalText = button.textContent; | ||||
|     button.classList.add('loading'); | ||||
| @@ -126,9 +131,18 @@ const elements = { | ||||
|   // Header elements | ||||
|   lockBtn: document.getElementById('lockBtn'), | ||||
|   themeToggle: document.getElementById('themeToggle'), | ||||
|   settingsToggle: document.getElementById('settingsToggle'), | ||||
|   settingsDropdown: document.getElementById('settingsDropdown'), | ||||
|   settingsBtn: document.getElementById('settingsBtn'), | ||||
|   headerTitle: document.getElementById('headerTitle'), | ||||
|  | ||||
|   // Section elements | ||||
|   authSection: document.getElementById('authSection'), | ||||
|   vaultSection: document.getElementById('vaultSection'), | ||||
|   settingsSection: document.getElementById('settingsSection'), | ||||
|  | ||||
|   // Settings page elements | ||||
|   timeoutInput: document.getElementById('timeoutInput'), | ||||
|   serverUrlInput: document.getElementById('serverUrlInput'), | ||||
|   saveServerUrlBtn: document.getElementById('saveServerUrlBtn'), | ||||
|  | ||||
|   // Keypair management elements | ||||
|   toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'), | ||||
| @@ -219,6 +233,53 @@ async function saveTimeoutSetting(timeout) { | ||||
|   await sendMessage('updateTimeout', { timeout }); | ||||
| } | ||||
|  | ||||
| // Server URL settings | ||||
| async function loadServerUrlSetting() { | ||||
|   try { | ||||
|     const result = await chrome.storage.local.get(['sigSocketUrl']); | ||||
|     const serverUrl = result.sigSocketUrl || 'ws://localhost:8080/ws'; | ||||
|     if (elements.serverUrlInput) { | ||||
|       elements.serverUrlInput.value = serverUrl; | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.warn('Failed to load server URL setting:', error); | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function saveServerUrlSetting() { | ||||
|   try { | ||||
|     const serverUrl = elements.serverUrlInput?.value?.trim(); | ||||
|     if (!serverUrl) { | ||||
|       showToast('Please enter a valid server URL', 'error'); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Basic URL validation | ||||
|     if (!serverUrl.startsWith('ws://') && !serverUrl.startsWith('wss://')) { | ||||
|       showToast('Server URL must start with ws:// or wss://', 'error'); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Save to storage | ||||
|     await chrome.storage.local.set({ sigSocketUrl: serverUrl }); | ||||
|  | ||||
|     // Notify background script to update server URL | ||||
|     const response = await sendMessage('updateSigSocketUrl', { serverUrl }); | ||||
|  | ||||
|     if (response?.success) { | ||||
|       showToast('Server URL saved successfully', 'success'); | ||||
|  | ||||
|       // Refresh connection status | ||||
|       await loadSigSocketState(); | ||||
|     } else { | ||||
|       showToast('Failed to update server URL', 'error'); | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.error('Failed to save server URL:', error); | ||||
|     showToast('Failed to save server URL', 'error'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function resetSessionTimeout() { | ||||
|   if (currentKeyspace) { | ||||
|     await sendMessage('resetTimeout'); | ||||
| @@ -241,11 +302,63 @@ function toggleTheme() { | ||||
|   updateThemeIcon(newTheme); | ||||
| } | ||||
|  | ||||
| // Settings dropdown management | ||||
| function toggleSettingsDropdown() { | ||||
|   const dropdown = elements.settingsDropdown; | ||||
|   if (dropdown) { | ||||
|     dropdown.classList.toggle('hidden'); | ||||
| // Settings page navigation | ||||
| async function showSettingsPage() { | ||||
|   // Hide all sections | ||||
|   document.querySelectorAll('.section').forEach(section => { | ||||
|     section.classList.add('hidden'); | ||||
|   }); | ||||
|  | ||||
|   // Show settings section | ||||
|   elements.settingsSection?.classList.remove('hidden'); | ||||
|  | ||||
|   // Ensure we have current status before updating settings display | ||||
|   await loadSigSocketState(); | ||||
| } | ||||
|  | ||||
| async function hideSettingsPage() { | ||||
|   // Hide settings section | ||||
|   elements.settingsSection?.classList.add('hidden'); | ||||
|  | ||||
|   // Check current session state to determine what to show | ||||
|   try { | ||||
|     const response = await sendMessage('getStatus'); | ||||
|  | ||||
|     if (response && response.success && response.status && response.session) { | ||||
|       // Active session exists - show vault section | ||||
|       currentKeyspace = response.session.keyspace; | ||||
|       if (elements.keyspaceInput) { | ||||
|         elements.keyspaceInput.value = currentKeyspace; | ||||
|       } | ||||
|       setStatus(currentKeyspace, true); | ||||
|       elements.vaultSection?.classList.remove('hidden'); | ||||
|       updateSettingsVisibility(); // Update settings visibility | ||||
|  | ||||
|       // Load vault content | ||||
|       await loadKeypairs(); | ||||
|  | ||||
|       // Use retry mechanism for existing sessions that might have stale connections | ||||
|       await loadSigSocketStateWithRetry(); | ||||
|     } else { | ||||
|       // No active session - show auth section | ||||
|       currentKeyspace = null; | ||||
|       setStatus('', false); | ||||
|       elements.authSection?.classList.remove('hidden'); | ||||
|       updateSettingsVisibility(); // Update settings visibility | ||||
|  | ||||
|       // For no session, use regular loading | ||||
|       await loadSigSocketState(); | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.warn('Failed to check session state:', error); | ||||
|     // Fallback to auth section on error | ||||
|     currentKeyspace = null; | ||||
|     setStatus('', false); | ||||
|     elements.authSection?.classList.remove('hidden'); | ||||
|     updateSettingsVisibility(); // Update settings visibility | ||||
|  | ||||
|     // Still try to load SigSocket state | ||||
|     await loadSigSocketState(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -287,6 +400,19 @@ function updateThemeIcon(theme) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Update settings button visibility based on keyspace state | ||||
| function updateSettingsVisibility() { | ||||
|   if (elements.settingsBtn) { | ||||
|     if (currentKeyspace) { | ||||
|       // Show settings when keyspace is unlocked | ||||
|       elements.settingsBtn.style.display = ''; | ||||
|     } else { | ||||
|       // Hide settings when keyspace is locked | ||||
|       elements.settingsBtn.style.display = 'none'; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Establish connection to background script for keep-alive | ||||
| function connectToBackground() { | ||||
|   backgroundPort = chrome.runtime.connect({ name: 'popup' }); | ||||
| @@ -299,6 +425,7 @@ function connectToBackground() { | ||||
|       selectedKeypairId = null; | ||||
|       setStatus('', false); | ||||
|       showSection('authSection'); | ||||
|       updateSettingsVisibility(); // Update settings visibility | ||||
|       clearVaultState(); | ||||
|  | ||||
|       // Clear form inputs | ||||
| @@ -313,6 +440,13 @@ function connectToBackground() { | ||||
|   backgroundPort.onDisconnect.addListener(() => { | ||||
|     backgroundPort = null; | ||||
|   }); | ||||
|  | ||||
|   // Immediately request status update when popup connects | ||||
|   setTimeout(() => { | ||||
|     if (backgroundPort) { | ||||
|       backgroundPort.postMessage({ type: 'REQUEST_IMMEDIATE_STATUS' }); | ||||
|     } | ||||
|   }, 50); // Small delay to ensure connection is established | ||||
| } | ||||
|  | ||||
| // Initialize | ||||
| @@ -323,6 +457,9 @@ document.addEventListener('DOMContentLoaded', async function() { | ||||
|   // Load timeout setting | ||||
|   await loadTimeoutSetting(); | ||||
|  | ||||
|   // Load server URL setting | ||||
|   await loadServerUrlSetting(); | ||||
|  | ||||
|   // Ensure lock button starts hidden | ||||
|   const lockBtn = document.getElementById('lockBtn'); | ||||
|   if (lockBtn) { | ||||
| @@ -338,7 +475,9 @@ document.addEventListener('DOMContentLoaded', async function() { | ||||
|     loginBtn: login, | ||||
|     lockBtn: lockSession, | ||||
|     themeToggle: toggleTheme, | ||||
|     settingsToggle: toggleSettingsDropdown, | ||||
|     settingsBtn: showSettingsPage, | ||||
|     headerTitle: hideSettingsPage, | ||||
|     saveServerUrlBtn: saveServerUrlSetting, | ||||
|     toggleAddKeypairBtn: toggleAddKeypairForm, | ||||
|     addKeypairBtn: addKeypair, | ||||
|     cancelAddKeypairBtn: hideAddKeypairForm, | ||||
| @@ -349,7 +488,10 @@ document.addEventListener('DOMContentLoaded', async function() { | ||||
|   }; | ||||
|  | ||||
|   Object.entries(eventMap).forEach(([elementKey, handler]) => { | ||||
|     elements[elementKey]?.addEventListener('click', handler); | ||||
|     const element = elements[elementKey]; | ||||
|     if (element) { | ||||
|       element.addEventListener('click', handler); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // Tab functionality | ||||
| @@ -400,10 +542,66 @@ document.addEventListener('DOMContentLoaded', async function() { | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // Initialize SigSocket UI elements after DOM is ready | ||||
|   sigSocketElements = { | ||||
|     connectionStatus: document.getElementById('connectionStatus'), | ||||
|     connectionDot: document.getElementById('connectionDot'), | ||||
|     connectionText: document.getElementById('connectionText'), | ||||
|     requestsContainer: document.getElementById('requestsContainer'), | ||||
|     loadingRequestsMessage: document.getElementById('loadingRequestsMessage'), | ||||
|     noRequestsMessage: document.getElementById('noRequestsMessage'), | ||||
|     requestsList: document.getElementById('requestsList'), | ||||
|     refreshRequestsBtn: document.getElementById('refreshRequestsBtn') | ||||
|   }; | ||||
|  | ||||
|   // Add SigSocket button listeners | ||||
|   sigSocketElements.refreshRequestsBtn?.addEventListener('click', refreshSigSocketRequests); | ||||
|  | ||||
|   // Check if opened via notification (focus on SigSocket section) | ||||
|   const urlParams = new URLSearchParams(window.location.search); | ||||
|   const fromNotification = urlParams.get('from') === 'notification'; | ||||
|  | ||||
|   // Check for existing session | ||||
|   await checkExistingSession(); | ||||
|  | ||||
|   // If opened from notification, focus on SigSocket section and show requests | ||||
|   if (fromNotification) { | ||||
|     console.log('🔔 Opened from notification, focusing on SigSocket section'); | ||||
|     focusOnSigSocketSection(); | ||||
|   } | ||||
|  | ||||
|   // Try to load any cached SigSocket state immediately for better UX | ||||
|   await loadCachedSigSocketState(); | ||||
| }); | ||||
|  | ||||
| // Focus on SigSocket section when opened from notification | ||||
| function focusOnSigSocketSection() { | ||||
|   try { | ||||
|     // Switch to SigSocket tab if not already active | ||||
|     const sigSocketTab = document.querySelector('[data-tab="sigsocket"]'); | ||||
|     if (sigSocketTab && !sigSocketTab.classList.contains('active')) { | ||||
|       sigSocketTab.click(); | ||||
|     } | ||||
|  | ||||
|     // Scroll to requests container | ||||
|     if (sigSocketElements.requestsContainer) { | ||||
|       sigSocketElements.requestsContainer.scrollIntoView({ | ||||
|         behavior: 'smooth', | ||||
|         block: 'start' | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // Show a helpful toast | ||||
|     showToast('New signature request received! Review pending requests below.', 'info'); | ||||
|  | ||||
|     // Refresh requests to ensure latest state | ||||
|     setTimeout(() => refreshSigSocketRequests(), 500); | ||||
|  | ||||
|   } catch (error) { | ||||
|     console.error('Failed to focus on SigSocket section:', error); | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function checkExistingSession() { | ||||
|   try { | ||||
|     const response = await sendMessage('getStatus'); | ||||
| @@ -413,15 +611,31 @@ async function checkExistingSession() { | ||||
|       elements.keyspaceInput.value = currentKeyspace; | ||||
|       setStatus(currentKeyspace, true); | ||||
|       showSection('vaultSection'); | ||||
|       updateSettingsVisibility(); // Update settings visibility | ||||
|       await loadKeypairs(); | ||||
|  | ||||
|       // Use retry mechanism for existing sessions to handle stale connections | ||||
|       await loadSigSocketStateWithRetry(); | ||||
|     } else { | ||||
|       // No active session | ||||
|       currentKeyspace = null; | ||||
|       setStatus('', false); | ||||
|       showSection('authSection'); | ||||
|       updateSettingsVisibility(); // Update settings visibility | ||||
|  | ||||
|       // For no session, use regular loading (no retry needed) | ||||
|       await loadSigSocketState(); | ||||
|     } | ||||
|   } catch (error) { | ||||
|     setStatus('', false); | ||||
|     showSection('authSection'); | ||||
|  | ||||
|     // Still try to load SigSocket state even on error | ||||
|     try { | ||||
|       await loadSigSocketState(); | ||||
|     } catch (sigSocketError) { | ||||
|       console.warn('Failed to load SigSocket state:', sigSocketError); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -641,9 +855,23 @@ async function login() { | ||||
|           currentKeyspace = auth.keyspace; | ||||
|           setStatus(auth.keyspace, true); | ||||
|           showSection('vaultSection'); | ||||
|           updateSettingsVisibility(); // Update settings visibility | ||||
|           clearVaultState(); | ||||
|           await loadKeypairs(); | ||||
|  | ||||
|           // Clean flow: Login -> Connect -> Restore -> Display | ||||
|           console.log('🔓 Login successful, applying clean flow...'); | ||||
|  | ||||
|           // 1. Wait for SigSocket to connect and restore requests | ||||
|           await loadSigSocketStateWithRetry(); | ||||
|  | ||||
|           // 2. Show loading state while fetching | ||||
|           showRequestsLoading(); | ||||
|  | ||||
|           // 3. Refresh requests to get the clean, restored state | ||||
|           await refreshSigSocketRequests(); | ||||
|  | ||||
|           console.log('✅ Login clean flow completed'); | ||||
|           return response; | ||||
|         } else { | ||||
|           throw new Error(getResponseError(response, 'login')); | ||||
| @@ -667,6 +895,7 @@ async function lockSession() { | ||||
|     selectedKeypairId = null; | ||||
|     setStatus('', false); | ||||
|     showSection('authSection'); | ||||
|     updateSettingsVisibility(); // Update settings visibility | ||||
|  | ||||
|     // Clear all form inputs | ||||
|     elements.keyspaceInput.value = ''; | ||||
| @@ -936,28 +1165,8 @@ const verifySignature = () => performCryptoOperation({ | ||||
| // SigSocket functionality | ||||
| let sigSocketRequests = []; | ||||
| let sigSocketStatus = { isConnected: false, workspace: null }; | ||||
|  | ||||
| // Initialize SigSocket UI elements | ||||
| const sigSocketElements = { | ||||
|   connectionStatus: document.getElementById('connectionStatus'), | ||||
|   connectionDot: document.getElementById('connectionDot'), | ||||
|   connectionText: document.getElementById('connectionText'), | ||||
|   requestsContainer: document.getElementById('requestsContainer'), | ||||
|   noRequestsMessage: document.getElementById('noRequestsMessage'), | ||||
|   requestsList: document.getElementById('requestsList'), | ||||
|   refreshRequestsBtn: document.getElementById('refreshRequestsBtn'), | ||||
|   sigSocketStatusBtn: document.getElementById('sigSocketStatusBtn') | ||||
| }; | ||||
|  | ||||
| // Add SigSocket event listeners | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|   // Add SigSocket button listeners | ||||
|   sigSocketElements.refreshRequestsBtn?.addEventListener('click', refreshSigSocketRequests); | ||||
|   sigSocketElements.sigSocketStatusBtn?.addEventListener('click', showSigSocketStatus); | ||||
|  | ||||
|   // Load initial SigSocket state | ||||
|   loadSigSocketState(); | ||||
| }); | ||||
| let sigSocketElements = {}; // Will be initialized in DOMContentLoaded | ||||
| let isInitialLoad = true; // Track if this is the first load | ||||
|  | ||||
| // Listen for messages from background script about SigSocket events | ||||
| if (backgroundPort) { | ||||
| @@ -968,6 +1177,12 @@ if (backgroundPort) { | ||||
|       updateRequestsList(message.pendingRequests); | ||||
|     } else if (message.type === 'KEYSPACE_UNLOCKED') { | ||||
|       handleKeypaceUnlocked(message); | ||||
|     } else if (message.type === 'CONNECTION_STATUS_CHANGED') { | ||||
|       handleConnectionStatusChanged(message); | ||||
|     } else if (message.type === 'FOCUS_SIGSOCKET') { | ||||
|       // Handle focus request from notification click | ||||
|       console.log('🔔 Received focus request from notification'); | ||||
|       focusOnSigSocketSection(); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| @@ -975,19 +1190,117 @@ if (backgroundPort) { | ||||
| // Load SigSocket state when popup opens | ||||
| async function loadSigSocketState() { | ||||
|   try { | ||||
|     // Get SigSocket status | ||||
|     const statusResponse = await sendMessage('getSigSocketStatus'); | ||||
|     if (statusResponse?.success) { | ||||
|       updateConnectionStatus(statusResponse.status); | ||||
|     console.log('🔄 Loading SigSocket state...'); | ||||
|  | ||||
|     // Show loading state for requests | ||||
|     showRequestsLoading(); | ||||
|  | ||||
|     // Show loading state for connection status on initial load | ||||
|     if (isInitialLoad) { | ||||
|       showConnectionLoading(); | ||||
|     } | ||||
|  | ||||
|     // Get pending requests | ||||
|     // Force a fresh connection status check with enhanced testing | ||||
|     const statusResponse = await sendMessage('getSigSocketStatusWithTest'); | ||||
|     if (statusResponse?.success) { | ||||
|       console.log('✅ Got SigSocket status:', statusResponse.status); | ||||
|       updateConnectionStatus(statusResponse.status); | ||||
|     } else { | ||||
|       console.warn('Enhanced status check failed, trying fallback...'); | ||||
|       // Fallback to regular status check | ||||
|       const fallbackResponse = await sendMessage('getSigSocketStatus'); | ||||
|       if (fallbackResponse?.success) { | ||||
|         console.log('✅ Got fallback SigSocket status:', fallbackResponse.status); | ||||
|         updateConnectionStatus(fallbackResponse.status); | ||||
|       } else { | ||||
|         // If both fail, show disconnected but don't show error on initial load | ||||
|         updateConnectionStatus({ | ||||
|           isConnected: false, | ||||
|           workspace: null, | ||||
|           publicKey: null, | ||||
|           pendingRequestCount: 0, | ||||
|           serverUrl: 'ws://localhost:8080/ws' | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Get pending requests - this now works even when keyspace is locked | ||||
|     console.log('📋 Fetching pending requests...'); | ||||
|     const requestsResponse = await sendMessage('getPendingSignRequests'); | ||||
|     if (requestsResponse?.success) { | ||||
|       console.log(`📋 Retrieved ${requestsResponse.requests?.length || 0} pending requests:`, requestsResponse.requests); | ||||
|       updateRequestsList(requestsResponse.requests); | ||||
|     } else { | ||||
|       console.warn('Failed to get pending requests:', requestsResponse); | ||||
|       updateRequestsList([]); | ||||
|     } | ||||
|  | ||||
|     // Mark initial load as complete | ||||
|     isInitialLoad = false; | ||||
|  | ||||
|   } catch (error) { | ||||
|     console.warn('Failed to load SigSocket state:', error); | ||||
|  | ||||
|     // Hide loading state and show error state | ||||
|     hideRequestsLoading(); | ||||
|  | ||||
|     // Set disconnected state on error (but don't show error toast on initial load) | ||||
|     updateConnectionStatus({ | ||||
|       isConnected: false, | ||||
|       workspace: null, | ||||
|       publicKey: null, | ||||
|       pendingRequestCount: 0, | ||||
|       serverUrl: 'ws://localhost:8080/ws' | ||||
|     }); | ||||
|  | ||||
|     // Still try to show any cached requests | ||||
|     updateRequestsList([]); | ||||
|  | ||||
|     // Mark initial load as complete | ||||
|     isInitialLoad = false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Load cached SigSocket state for immediate display | ||||
| async function loadCachedSigSocketState() { | ||||
|   try { | ||||
|     // Try to get any cached requests from storage for immediate display | ||||
|     const cachedData = await chrome.storage.local.get(['sigSocketPendingRequests']); | ||||
|     if (cachedData.sigSocketPendingRequests && Array.isArray(cachedData.sigSocketPendingRequests)) { | ||||
|       console.log('📋 Loading cached requests for immediate display'); | ||||
|       updateRequestsList(cachedData.sigSocketPendingRequests); | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.warn('Failed to load cached SigSocket state:', error); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Load SigSocket state with simple retry for session initialization timing | ||||
| async function loadSigSocketStateWithRetry() { | ||||
|   // First try immediately (might already be connected) | ||||
|   await loadSigSocketState(); | ||||
|  | ||||
|   // If still showing disconnected after initial load, try again after a short delay | ||||
|   if (!sigSocketStatus.isConnected) { | ||||
|     console.log('🔄 Initial load showed disconnected, retrying after delay...'); | ||||
|     await new Promise(resolve => setTimeout(resolve, 500)); | ||||
|     await loadSigSocketState(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Show loading state for connection status | ||||
| function showConnectionLoading() { | ||||
|   if (sigSocketElements.connectionDot && sigSocketElements.connectionText) { | ||||
|     sigSocketElements.connectionDot.classList.remove('connected'); | ||||
|     sigSocketElements.connectionDot.classList.add('loading'); | ||||
|     sigSocketElements.connectionText.textContent = 'Checking...'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Hide loading state for connection status | ||||
| function hideConnectionLoading() { | ||||
|   if (sigSocketElements.connectionDot) { | ||||
|     sigSocketElements.connectionDot.classList.remove('loading'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -995,15 +1308,42 @@ async function loadSigSocketState() { | ||||
| function updateConnectionStatus(status) { | ||||
|   sigSocketStatus = status; | ||||
|  | ||||
|   // Hide loading state | ||||
|   hideConnectionLoading(); | ||||
|  | ||||
|   if (sigSocketElements.connectionDot && sigSocketElements.connectionText) { | ||||
|     if (status.isConnected) { | ||||
|       sigSocketElements.connectionDot.classList.add('connected'); | ||||
|       sigSocketElements.connectionText.textContent = `Connected (${status.workspace || 'Unknown'})`; | ||||
|       sigSocketElements.connectionText.textContent = 'Connected'; | ||||
|     } else { | ||||
|       sigSocketElements.connectionDot.classList.remove('connected'); | ||||
|       sigSocketElements.connectionText.textContent = 'Disconnected'; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Log connection details for debugging | ||||
|   console.log('🔗 Connection status updated:', { | ||||
|     connected: status.isConnected, | ||||
|     workspace: status.workspace, | ||||
|     publicKey: status.publicKey?.substring(0, 16) + '...', | ||||
|     serverUrl: status.serverUrl | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Show loading state for requests | ||||
| function showRequestsLoading() { | ||||
|   if (!sigSocketElements.requestsContainer) return; | ||||
|  | ||||
|   sigSocketElements.loadingRequestsMessage?.classList.remove('hidden'); | ||||
|   sigSocketElements.noRequestsMessage?.classList.add('hidden'); | ||||
|   sigSocketElements.requestsList?.classList.add('hidden'); | ||||
| } | ||||
|  | ||||
| // Hide loading state for requests | ||||
| function hideRequestsLoading() { | ||||
|   if (!sigSocketElements.requestsContainer) return; | ||||
|  | ||||
|   sigSocketElements.loadingRequestsMessage?.classList.add('hidden'); | ||||
| } | ||||
|  | ||||
| // Update requests list display | ||||
| @@ -1012,6 +1352,9 @@ function updateRequestsList(requests) { | ||||
|  | ||||
|   if (!sigSocketElements.requestsContainer) return; | ||||
|  | ||||
|   // Hide loading state | ||||
|   hideRequestsLoading(); | ||||
|  | ||||
|   if (sigSocketRequests.length === 0) { | ||||
|     sigSocketElements.noRequestsMessage?.classList.remove('hidden'); | ||||
|     sigSocketElements.requestsList?.classList.add('hidden'); | ||||
| @@ -1036,8 +1379,42 @@ function createRequestItem(request) { | ||||
|   const shortId = request.id.substring(0, 8) + '...'; | ||||
|   const decodedMessage = request.message ? atob(request.message) : 'No message'; | ||||
|  | ||||
|   // Check if keyspace is currently unlocked | ||||
|   const isKeypaceUnlocked = currentKeyspace !== null; | ||||
|  | ||||
|   // Create different UI based on keyspace lock status | ||||
|   let actionsHtml; | ||||
|   let statusIndicator = ''; | ||||
|  | ||||
|   if (isKeypaceUnlocked) { | ||||
|     // Normal approve/reject buttons when unlocked | ||||
|     actionsHtml = ` | ||||
|       <div class="request-actions"> | ||||
|         <button class="btn-approve" data-request-id="${request.id}"> | ||||
|           ✓ Approve | ||||
|         </button> | ||||
|         <button class="btn-reject" data-request-id="${request.id}"> | ||||
|           ✗ Reject | ||||
|         </button> | ||||
|       </div> | ||||
|     `; | ||||
|   } else { | ||||
|     // Show pending status and unlock message when locked | ||||
|     statusIndicator = '<div class="request-status pending">⏳ Pending - Unlock keyspace to approve/reject</div>'; | ||||
|     actionsHtml = ` | ||||
|       <div class="request-actions locked"> | ||||
|         <button class="btn-approve" data-request-id="${request.id}" disabled title="Unlock keyspace to approve"> | ||||
|           ✓ Approve | ||||
|         </button> | ||||
|         <button class="btn-reject" data-request-id="${request.id}" disabled title="Unlock keyspace to reject"> | ||||
|           ✗ Reject | ||||
|         </button> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   return ` | ||||
|     <div class="request-item" data-request-id="${request.id}"> | ||||
|     <div class="request-item ${isKeypaceUnlocked ? '' : 'locked'}" data-request-id="${request.id}"> | ||||
|       <div class="request-header"> | ||||
|         <div class="request-id" title="${request.id}">${shortId}</div> | ||||
|         <div class="request-time">${requestTime}</div> | ||||
| @@ -1047,14 +1424,8 @@ function createRequestItem(request) { | ||||
|         ${decodedMessage.length > 100 ? decodedMessage.substring(0, 100) + '...' : decodedMessage} | ||||
|       </div> | ||||
|  | ||||
|       <div class="request-actions"> | ||||
|         <button class="btn-approve" data-request-id="${request.id}"> | ||||
|           ✓ Approve | ||||
|         </button> | ||||
|         <button class="btn-reject" data-request-id="${request.id}"> | ||||
|           ✗ Reject | ||||
|         </button> | ||||
|       </div> | ||||
|       ${statusIndicator} | ||||
|       ${actionsHtml} | ||||
|     </div> | ||||
|   `; | ||||
| } | ||||
| @@ -1091,15 +1462,61 @@ function handleNewSignRequest(message) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Handle keyspace unlocked event | ||||
| // Handle keyspace unlocked event - Clean flow implementation | ||||
| function handleKeypaceUnlocked(message) { | ||||
|   // Update requests list | ||||
|   if (message.pendingRequests) { | ||||
|     updateRequestsList(message.pendingRequests); | ||||
|   } | ||||
|   console.log('🔓 Keyspace unlocked - applying clean flow for request display'); | ||||
|  | ||||
|   // Update button states based on whether requests can be approved | ||||
|   updateRequestButtonStates(message.canApprove); | ||||
|   // Clean flow: Unlock -> Show loading -> Display requests -> Update UI | ||||
|   try { | ||||
|     // 1. Show loading state immediately | ||||
|     showRequestsLoading(); | ||||
|  | ||||
|     // 2. Update requests list with restored + current requests | ||||
|     if (message.pendingRequests && Array.isArray(message.pendingRequests)) { | ||||
|       console.log(`📋 Displaying ${message.pendingRequests.length} restored requests`); | ||||
|       updateRequestsList(message.pendingRequests); | ||||
|  | ||||
|       // 3. Update button states (should be enabled now) | ||||
|       updateRequestButtonStates(message.canApprove !== false); | ||||
|  | ||||
|       // 4. Show appropriate notification | ||||
|       const count = message.pendingRequests.length; | ||||
|       if (count > 0) { | ||||
|         showToast(`Keyspace unlocked! ${count} pending request${count > 1 ? 's' : ''} ready for review.`, 'info'); | ||||
|       } else { | ||||
|         showToast('Keyspace unlocked! No pending requests.', 'success'); | ||||
|       } | ||||
|     } else { | ||||
|       // 5. If no requests in message, fetch fresh from server | ||||
|       console.log('📋 No requests in unlock message, fetching from server...'); | ||||
|       setTimeout(() => refreshSigSocketRequests(), 100); | ||||
|     } | ||||
|  | ||||
|     console.log('✅ Keyspace unlock flow completed'); | ||||
|  | ||||
|   } catch (error) { | ||||
|     console.error('❌ Error in keyspace unlock flow:', error); | ||||
|     hideRequestsLoading(); | ||||
|     showToast('Error loading requests after unlock', 'error'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Handle connection status change event | ||||
| function handleConnectionStatusChanged(message) { | ||||
|   console.log('🔄 Connection status changed:', message.status); | ||||
|  | ||||
|   // Store previous state for comparison | ||||
|   const previousState = sigSocketStatus ? sigSocketStatus.isConnected : null; | ||||
|  | ||||
|   // Update the connection status display | ||||
|   updateConnectionStatus(message.status); | ||||
|  | ||||
|   // Only show toast for actual changes, not initial status, and not during initial load | ||||
|   if (!isInitialLoad && previousState !== null && previousState !== message.status.isConnected) { | ||||
|     const statusText = message.status.isConnected ? 'Connected' : 'Disconnected'; | ||||
|     const toastType = message.status.isConnected ? 'success' : 'warning'; | ||||
|     showToast(`SigSocket ${statusText}`, toastType); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Show workspace mismatch warning | ||||
| @@ -1133,30 +1550,46 @@ function updateRequestButtonStates(canApprove) { | ||||
|  | ||||
| // Approve a sign request | ||||
| async function approveSignRequest(requestId) { | ||||
|   let button = null; | ||||
|   try { | ||||
|     const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); | ||||
|     button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); | ||||
|     setButtonLoading(button, true); | ||||
|  | ||||
|     const response = await sendMessage('approveSignRequest', { requestId }); | ||||
|  | ||||
|     if (response?.success) { | ||||
|       showToast('Request approved and signed!', 'success'); | ||||
|       showRequestsLoading(); | ||||
|       await refreshSigSocketRequests(); | ||||
|     } else { | ||||
|       throw new Error(getResponseError(response, 'approve request')); | ||||
|       const errorMsg = getResponseError(response, 'approve request'); | ||||
|  | ||||
|       // Check for specific connection errors | ||||
|       if (errorMsg.includes('Connection not found') || errorMsg.includes('public key')) { | ||||
|         showToast('Connection error: Please check SigSocket connection and try again', 'error'); | ||||
|         // Trigger a connection status refresh | ||||
|         await loadSigSocketState(); | ||||
|       } else if (errorMsg.includes('keyspace') || errorMsg.includes('locked')) { | ||||
|         showToast('Keyspace is locked. Please unlock to approve requests.', 'error'); | ||||
|       } else { | ||||
|         throw new Error(errorMsg); | ||||
|       } | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.error('Error approving request:', error); | ||||
|     showToast(`Failed to approve request: ${error.message}`, 'error'); | ||||
|   } finally { | ||||
|     const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); | ||||
|     setButtonLoading(button, false); | ||||
|     // Re-query button in case DOM was updated during the operation | ||||
|     const finalButton = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); | ||||
|     setButtonLoading(finalButton, false); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Reject a sign request | ||||
| async function rejectSignRequest(requestId) { | ||||
|   let button = null; | ||||
|   try { | ||||
|     const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); | ||||
|     button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); | ||||
|     setButtonLoading(button, true); | ||||
|  | ||||
|     const response = await sendMessage('rejectSignRequest', { | ||||
| @@ -1166,6 +1599,7 @@ async function rejectSignRequest(requestId) { | ||||
|  | ||||
|     if (response?.success) { | ||||
|       showToast('Request rejected', 'info'); | ||||
|       showRequestsLoading(); | ||||
|       await refreshSigSocketRequests(); | ||||
|     } else { | ||||
|       throw new Error(getResponseError(response, 'reject request')); | ||||
| @@ -1173,8 +1607,9 @@ async function rejectSignRequest(requestId) { | ||||
|   } catch (error) { | ||||
|     showToast(`Failed to reject request: ${error.message}`, 'error'); | ||||
|   } finally { | ||||
|     const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); | ||||
|     setButtonLoading(button, false); | ||||
|     // Re-query button in case DOM was updated during the operation | ||||
|     const finalButton = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); | ||||
|     setButtonLoading(finalButton, false); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -1182,42 +1617,35 @@ async function rejectSignRequest(requestId) { | ||||
| async function refreshSigSocketRequests() { | ||||
|   try { | ||||
|     setButtonLoading(sigSocketElements.refreshRequestsBtn, true); | ||||
|     showRequestsLoading(); | ||||
|  | ||||
|     console.log('🔄 Refreshing SigSocket requests...'); | ||||
|     const response = await sendMessage('getPendingSignRequests'); | ||||
|  | ||||
|     if (response?.success) { | ||||
|       console.log(`📋 Retrieved ${response.requests?.length || 0} pending requests`); | ||||
|       updateRequestsList(response.requests); | ||||
|       showToast('Requests refreshed', 'success'); | ||||
|  | ||||
|       const count = response.requests?.length || 0; | ||||
|       if (count > 0) { | ||||
|         showToast(`${count} pending request${count > 1 ? 's' : ''} found`, 'success'); | ||||
|       } else { | ||||
|         showToast('No pending requests', 'info'); | ||||
|       } | ||||
|     } else { | ||||
|       console.error('Failed to get pending requests:', response); | ||||
|       hideRequestsLoading(); | ||||
|       throw new Error(getResponseError(response, 'refresh requests')); | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.error('Error refreshing requests:', error); | ||||
|     hideRequestsLoading(); | ||||
|     showToast(`Failed to refresh requests: ${error.message}`, 'error'); | ||||
|   } finally { | ||||
|     setButtonLoading(sigSocketElements.refreshRequestsBtn, false); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Show SigSocket status | ||||
| async function showSigSocketStatus() { | ||||
|   try { | ||||
|     const response = await sendMessage('getSigSocketStatus'); | ||||
|     if (response?.success) { | ||||
|       const status = response.status; | ||||
|       const statusText = ` | ||||
| SigSocket Status: | ||||
| • Connected: ${status.isConnected ? 'Yes' : 'No'} | ||||
| • Workspace: ${status.workspace || 'None'} | ||||
| • Public Key: ${status.publicKey ? status.publicKey.substring(0, 16) + '...' : 'None'} | ||||
| • Pending Requests: ${status.pendingRequestCount || 0} | ||||
| • Server URL: ${status.serverUrl} | ||||
|       `.trim(); | ||||
|  | ||||
|       showToast(statusText, 'info'); | ||||
|       updateConnectionStatus(status); | ||||
|     } else { | ||||
|       throw new Error(getResponseError(response, 'get status')); | ||||
|     } | ||||
|   } catch (error) { | ||||
|     showToast(`Failed to get status: ${error.message}`, 'error'); | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -188,6 +188,15 @@ body { | ||||
|   margin: 0; | ||||
| } | ||||
|  | ||||
| .clickable-header { | ||||
|   cursor: pointer; | ||||
|   transition: opacity 0.2s ease; | ||||
| } | ||||
|  | ||||
| .clickable-header:hover { | ||||
|   opacity: 0.8; | ||||
| } | ||||
|  | ||||
| .header-actions { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
| @@ -261,6 +270,75 @@ body { | ||||
|   color: var(--text-muted); | ||||
| } | ||||
|  | ||||
| .server-input-group { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: var(--spacing-sm); | ||||
| } | ||||
|  | ||||
| .server-input-group input { | ||||
|   flex: 1; | ||||
|   padding: var(--spacing-xs) var(--spacing-sm); | ||||
|   font-size: 14px; | ||||
|   border: 1px solid var(--border-color); | ||||
|   border-radius: 8px; | ||||
|   background: var(--bg-input); | ||||
|   color: var(--text-primary); | ||||
|   font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | ||||
| } | ||||
|  | ||||
| .server-input-group input:focus { | ||||
|   outline: none; | ||||
|   border-color: var(--border-focus); | ||||
|   box-shadow: 0 0 0 2px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.15); | ||||
| } | ||||
|  | ||||
| .settings-help { | ||||
|   display: block; | ||||
|   font-size: 12px; | ||||
|   color: var(--text-muted); | ||||
|   margin-top: var(--spacing-xs); | ||||
|   font-style: italic; | ||||
| } | ||||
|  | ||||
| /* Settings page styles */ | ||||
|  | ||||
| .settings-header { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: var(--spacing-md); | ||||
|   margin-bottom: var(--spacing-lg); | ||||
| } | ||||
|  | ||||
| .settings-header h2 { | ||||
|   margin: 0; | ||||
|   font-size: 20px; | ||||
|   font-weight: 600; | ||||
|   color: var(--text-primary); | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| .about-info { | ||||
|   text-align: left; | ||||
| } | ||||
|  | ||||
| .about-info p { | ||||
|   margin: 0 0 var(--spacing-xs) 0; | ||||
|   font-size: 14px; | ||||
|   color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| .about-info strong { | ||||
|   font-weight: 600; | ||||
| } | ||||
|  | ||||
| .version-info { | ||||
|   font-size: 12px; | ||||
|   color: var(--text-muted); | ||||
|   font-style: italic; | ||||
| } | ||||
|  | ||||
| .btn-icon-only { | ||||
|   background: var(--bg-button-ghost); | ||||
|   border: none; | ||||
| @@ -456,6 +534,17 @@ input::placeholder, textarea::placeholder { | ||||
|   font-size: 12px; | ||||
| } | ||||
|  | ||||
| /* Button icon spacing */ | ||||
| .btn svg { | ||||
|   margin-right: var(--spacing-xs); | ||||
|   flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| .btn svg:last-child { | ||||
|   margin-right: 0; | ||||
|   margin-left: var(--spacing-xs); | ||||
| } | ||||
|  | ||||
| .btn:disabled { | ||||
|   opacity: 0.5; | ||||
|   cursor: not-allowed; | ||||
| @@ -1073,18 +1162,19 @@ input::placeholder, textarea::placeholder { | ||||
|  | ||||
| /* SigSocket Requests Styles */ | ||||
| .sigsocket-section { | ||||
|   margin-bottom: 20px; | ||||
|   margin-bottom: var(--spacing-lg); | ||||
| } | ||||
|  | ||||
| .section-header { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   margin-bottom: 15px; | ||||
|   margin-bottom: var(--spacing-lg); | ||||
| } | ||||
|  | ||||
| .section-header h3 { | ||||
|   margin: 0; | ||||
|   color: var(--text-primary); | ||||
|   font-size: 16px; | ||||
|   font-weight: 600; | ||||
| } | ||||
| @@ -1092,7 +1182,7 @@ input::placeholder, textarea::placeholder { | ||||
| .connection-status { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 6px; | ||||
|   gap: var(--spacing-xs); | ||||
|   font-size: 12px; | ||||
|   color: var(--text-secondary); | ||||
| } | ||||
| @@ -1109,16 +1199,62 @@ input::placeholder, textarea::placeholder { | ||||
|   background: var(--accent-success); | ||||
| } | ||||
|  | ||||
| .status-dot.loading { | ||||
|   background: var(--accent-warning); | ||||
|   animation: pulse-dot 1.5s ease-in-out infinite; | ||||
| } | ||||
|  | ||||
| @keyframes pulse-dot { | ||||
|   0%, 100% { | ||||
|     opacity: 1; | ||||
|     transform: scale(1); | ||||
|   } | ||||
|   50% { | ||||
|     opacity: 0.6; | ||||
|     transform: scale(1.2); | ||||
|   } | ||||
| } | ||||
|  | ||||
| .requests-container { | ||||
|   min-height: 80px; | ||||
| } | ||||
|  | ||||
| .empty-state { | ||||
|   text-align: center; | ||||
|   padding: 20px; | ||||
|   padding: var(--spacing-xl); | ||||
|   color: var(--text-secondary); | ||||
| } | ||||
|  | ||||
| .loading-state { | ||||
|   text-align: center; | ||||
|   padding: var(--spacing-xl); | ||||
|   color: var(--text-secondary); | ||||
| } | ||||
|  | ||||
| .loading-spinner { | ||||
|   width: 24px; | ||||
|   height: 24px; | ||||
|   border: 2px solid var(--border-color); | ||||
|   border-top: 2px solid var(--primary-color); | ||||
|   border-radius: 50%; | ||||
|   animation: spin 1s linear infinite; | ||||
|   margin: 0 auto var(--spacing-sm) auto; | ||||
| } | ||||
|  | ||||
| @keyframes spin { | ||||
|   0% { transform: rotate(0deg); } | ||||
|   100% { transform: rotate(360deg); } | ||||
| } | ||||
|  | ||||
| .loading-state p { | ||||
|   margin: var(--spacing-sm) 0; | ||||
|   font-weight: 500; | ||||
| } | ||||
|  | ||||
| .loading-state small { | ||||
|   opacity: 0.8; | ||||
| } | ||||
|  | ||||
| .empty-icon { | ||||
|   font-size: 24px; | ||||
|   margin-bottom: 8px; | ||||
| @@ -1228,10 +1364,42 @@ input::placeholder, textarea::placeholder { | ||||
|  | ||||
| .sigsocket-actions { | ||||
|   display: flex; | ||||
|   gap: 8px; | ||||
|   margin-top: 12px; | ||||
|   padding-top: 12px; | ||||
|   border-top: 1px solid var(--border-color); | ||||
|   gap: var(--spacing-sm); | ||||
|   margin-top: var(--spacing-md); | ||||
| } | ||||
|  | ||||
| /* Ensure refresh button follows design system */ | ||||
| #refreshRequestsBtn { | ||||
|   display: inline-flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   gap: var(--spacing-xs); | ||||
| } | ||||
|  | ||||
| /* Request item locked state styles */ | ||||
| .request-item.locked { | ||||
|   opacity: 0.8; | ||||
|   border-left: 3px solid var(--warning-color, #ffa500); | ||||
| } | ||||
|  | ||||
| .request-status.pending { | ||||
|   background: var(--warning-bg, #fff3cd); | ||||
|   color: var(--warning-text, #856404); | ||||
|   padding: var(--spacing-xs) var(--spacing-sm); | ||||
|   border-radius: 4px; | ||||
|   font-size: 12px; | ||||
|   margin: var(--spacing-xs) 0; | ||||
|   border: 1px solid var(--warning-border, #ffeaa7); | ||||
| } | ||||
|  | ||||
| .request-actions.locked button { | ||||
|   opacity: 0.6; | ||||
|   cursor: not-allowed; | ||||
| } | ||||
|  | ||||
| .request-actions.locked button:hover { | ||||
|   background: var(--button-bg) !important; | ||||
|   transform: none !important; | ||||
| } | ||||
|  | ||||
| .workspace-mismatch { | ||||
|   | ||||
| @@ -467,31 +467,31 @@ export function run_rhai(script) { | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_34(arg0, arg1, arg2) { | ||||
|     wasm.closure174_externref_shim(arg0, arg1, arg2); | ||||
|     wasm.closure203_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_39(arg0, arg1) { | ||||
|     wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha4436a3f79fb1a0f(arg0, arg1); | ||||
|     wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd79bf9f6d48e92f7(arg0, arg1); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_44(arg0, arg1, arg2) { | ||||
|     wasm.closure237_externref_shim(arg0, arg1, arg2); | ||||
|     wasm.closure239_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_49(arg0, arg1) { | ||||
|     wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf148c54a4a246cea(arg0, arg1); | ||||
|     wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf103de07b8856532(arg0, arg1); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_52(arg0, arg1, arg2) { | ||||
|     wasm.closure308_externref_shim(arg0, arg1, arg2); | ||||
|     wasm.closure319_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_55(arg0, arg1, arg2) { | ||||
|     wasm.closure392_externref_shim(arg0, arg1, arg2); | ||||
|     wasm.closure395_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_207(arg0, arg1, arg2, arg3) { | ||||
|     wasm.closure2046_externref_shim(arg0, arg1, arg2, arg3); | ||||
|     wasm.closure2042_externref_shim(arg0, arg1, arg2, arg3); | ||||
| } | ||||
|  | ||||
| const __wbindgen_enum_BinaryType = ["blob", "arraybuffer"]; | ||||
| @@ -1217,40 +1217,40 @@ function __wbg_get_imports() { | ||||
|         const ret = false; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper1015 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 309, __wbg_adapter_52); | ||||
|     imports.wbg.__wbindgen_closure_wrapper1036 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 320, __wbg_adapter_52); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper1320 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 393, __wbg_adapter_55); | ||||
|     imports.wbg.__wbindgen_closure_wrapper1329 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 396, __wbg_adapter_55); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper423 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); | ||||
|     imports.wbg.__wbindgen_closure_wrapper624 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper424 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); | ||||
|     imports.wbg.__wbindgen_closure_wrapper625 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper425 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_39); | ||||
|     imports.wbg.__wbindgen_closure_wrapper626 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_39); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper428 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); | ||||
|     imports.wbg.__wbindgen_closure_wrapper630 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper765 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper766 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44); | ||||
|         const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper767 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper770 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_49); | ||||
|     imports.wbg.__wbindgen_closure_wrapper768 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_49); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user