background.ts
1 import { defineBackground } from 'wxt/sandbox'; 2 import { storage } from '../lib/storage'; 3 import { workspaceManager, repairHierarchy, needsMigration, migrateConfig } from '../lib/workspace'; 4 import { 5 getWindowTracker, 6 switchContext, 7 quickSwitchToWorkspace, 8 closeWorkspaceWindows, 9 captureWindowTabs, 10 captureWindowPosition, 11 refreshWorkspaceFingerprint, 12 findDashboardTabInWindow, 13 getContextSwitchConfirmation, 14 } from '../lib/window'; 15 import { 16 createSearchIndex, 17 buildIndex, 18 updateWorkspaceInIndex, 19 removeWorkspaceFromIndex, 20 search, 21 getSuggestions as getSearchSuggestions, 22 quickSearch, 23 serializeIndex, 24 deserializeIndex, 25 type SearchIndex, 26 } from '../lib/search'; 27 import { 28 getClaudeClient, 29 getSuggestionEngine, 30 } from '../lib/claude'; 31 import { 32 isGenericTitle, 33 extractPageContent, 34 buildEnhancedDescription, 35 type EnrichedTabInfo, 36 type TabEnrichmentResult, 37 } from '../lib/claude/tab-content-extractor'; 38 import { 39 getSyncManager, 40 getRestBridge, 41 DEFAULT_SHARED_FILE, 42 type SyncBackendType, 43 type SharedWorkspaceFile, 44 type SharedWorkspace, 45 type BrowserKey, 46 type BrowserWorkspaces, 47 type TabTransfer, 48 } from '../lib/sync'; 49 import { getDebugLogger } from '../lib/debug'; 50 import type { ContextSwitchRequest, TabWorkspace, SearchOptions, ClaudeSettings, ChildWorkspace, ParentWorkspace, StandaloneWorkspace } from '../lib/types'; 51 import { isTabWorkspace, DEFAULT_CLAUDE_SETTINGS } from '../lib/types'; 52 import { 53 exportWorkspaceToBookmarks, 54 exportContextToBookmarks, 55 getMnemonicBookmarkFolders, 56 detectStructure, 57 importAsStandalone, 58 importAsContext, 59 getRecommendedImportMode, 60 } from '../lib/bookmarks'; 61 62 // Claude settings cache 63 let claudeSettings: ClaudeSettings = { ...DEFAULT_CLAUDE_SETTINGS }; 64 65 // Global search index 66 let searchIndex: SearchIndex = createSearchIndex(); 67 68 // Search index storage key 69 const SEARCH_INDEX_STORAGE_KEY = 'searchIndexCache'; 70 71 // Claude settings storage key 72 const CLAUDE_SETTINGS_STORAGE_KEY = 'claudeSettings'; 73 74 // Debounce timer for saving search index 75 let saveIndexTimer: ReturnType<typeof setTimeout> | null = null; 76 77 // Debounce timers for auto-save per window 78 const autoSaveTimers: Map<number, ReturnType<typeof setTimeout>> = new Map(); 79 const AUTO_SAVE_DEBOUNCE_MS = 2000; // 2 seconds debounce 80 81 // Storage change listener unsubscribe function (to prevent FD leaks from duplicate registrations) 82 let storageListenerUnsubscribe: (() => void) | null = null; 83 84 // Flag to block auto-save and sync until initial external sync is complete 85 // This prevents overwriting external data before we've had a chance to pull it 86 let initialSyncComplete = false; 87 88 /** 89 * Broadcast workspace status changes to all dashboard instances 90 */ 91 function broadcastWorkspaceStatusChange(): void { 92 browser.runtime.sendMessage({ type: 'WORKSPACE_STATUS_CHANGED' }).catch(() => { 93 // Ignore errors - UI might not be listening 94 }); 95 } 96 97 /** 98 * Broadcast audible state changes to all dashboard instances 99 * This is called when a tab's audible state changes, enabling event-based updates 100 * instead of polling for audible tabs 101 */ 102 function broadcastAudibleStateChange(): void { 103 browser.runtime.sendMessage({ type: 'AUDIBLE_STATE_CHANGED' }).catch(() => { 104 // Ignore errors - UI might not be listening 105 }); 106 } 107 108 /** 109 * Detect current browser for cross-browser workspace sharing 110 */ 111 function detectBrowserKey(): BrowserKey { 112 // In service worker context, we need to check browser-specific APIs 113 // Firefox has browser.runtime.getBrowserInfo(), Chrome doesn't 114 if (typeof browser !== 'undefined' && 'getBrowserInfo' in browser.runtime) { 115 return 'firefox'; 116 } 117 // Check for Chrome-specific behavior 118 if (typeof chrome !== 'undefined' && chrome.runtime && !('getBrowserInfo' in chrome.runtime)) { 119 return 'chrome'; 120 } 121 return 'unknown'; 122 } 123 124 // ============================================================================= 125 // Cross-Browser Tab Transfer Polling 126 // ============================================================================= 127 128 let transferPollInterval: ReturnType<typeof setInterval> | null = null; 129 130 /** 131 * Start polling the bridge for incoming tab transfers. 132 * Runs every 5 seconds while bridge is connected. 133 */ 134 function startTransferPolling(): void { 135 if (transferPollInterval) return; 136 137 const browserKey = detectBrowserKey(); 138 const bridge = getRestBridge(); 139 140 transferPollInterval = setInterval(async () => { 141 try { 142 if (!bridge.isConnected()) return; 143 const result = await bridge.pollTransfers(browserKey); 144 if (result.transfers && result.transfers.length > 0) { 145 for (const transfer of result.transfers) { 146 await processIncomingTransfer(transfer); 147 } 148 } 149 } catch { 150 // Silent — bridge may be temporarily down 151 } 152 }, 5000); 153 154 console.log('[Mnemonic] Transfer polling started for', browserKey); 155 } 156 157 /** 158 * Process a single incoming tab transfer. 159 * Adds the tab to the target workspace and opens it if the window is active. 160 */ 161 async function processIncomingTransfer(transfer: TabTransfer): Promise<void> { 162 const { target_workspace_id, target_workspace_name, tab } = transfer; 163 164 try { 165 // 1. Look up workspace 166 const workspace = workspaceManager.getById(target_workspace_id); 167 if (!workspace) { 168 console.warn('[Mnemonic:Transfer] Workspace not found:', target_workspace_id, target_workspace_name); 169 return; 170 } 171 172 // 2. Must be a tab workspace 173 if (!isTabWorkspace(workspace)) { 174 console.warn('[Mnemonic:Transfer] Target is not a tab workspace:', target_workspace_name); 175 return; 176 } 177 178 // 3. Dedup — skip if URL already exists in workspace 179 const existingTabs = workspace.tabs || []; 180 if (existingTabs.some(t => t.url === tab.url)) { 181 console.log('[Mnemonic:Transfer] Tab already in workspace, skipping:', tab.url); 182 return; 183 } 184 185 // 4. Add tab to workspace 186 const newTab = { url: tab.url, title: tab.title, pinned: tab.pinned || false }; 187 await workspaceManager.updateTabs(target_workspace_id, [...existingTabs, newTab]); 188 console.log('[Mnemonic:Transfer] Added tab to', target_workspace_name, ':', tab.url); 189 190 // 5. If workspace has an open window, create the tab there and focus 191 const windowTracker = getWindowTracker(); 192 const windowId = windowTracker.getWorkspaceWindow(target_workspace_id); 193 if (windowId) { 194 try { 195 await browser.tabs.create({ 196 windowId, 197 url: tab.url, 198 pinned: tab.pinned || false, 199 active: false, 200 }); 201 await browser.windows.update(windowId, { focused: true }); 202 console.log('[Mnemonic:Transfer] Opened tab in window', windowId); 203 } catch (e) { 204 console.warn('[Mnemonic:Transfer] Could not open tab in window:', e); 205 } 206 } 207 208 // 6. Notify UI 209 broadcastWorkspaceStatusChange(); 210 } catch (e) { 211 console.error('[Mnemonic:Transfer] Error processing transfer:', e); 212 } 213 } 214 215 /** 216 * Save Claude settings to storage 217 */ 218 async function saveClaudeSettings(): Promise<void> { 219 try { 220 await browser.storage.local.set({ 221 [CLAUDE_SETTINGS_STORAGE_KEY]: claudeSettings, 222 }); 223 console.log('[Mnemonic] Claude settings saved'); 224 } catch (e) { 225 console.error('[Mnemonic] Failed to save Claude settings:', e); 226 } 227 } 228 229 /** 230 * Show or clear error badge on extension icon for Claude API failures 231 */ 232 function setClaudeErrorBadge(show: boolean): void { 233 browser.action.setBadgeText({ text: show ? '!' : '' }); 234 browser.action.setBadgeBackgroundColor({ color: '#EF4444' }); 235 } 236 237 /** 238 * Load Claude settings from storage 239 */ 240 async function loadClaudeSettings(): Promise<void> { 241 try { 242 const result = await browser.storage.local.get(CLAUDE_SETTINGS_STORAGE_KEY); 243 const stored = result[CLAUDE_SETTINGS_STORAGE_KEY]; 244 245 if (stored) { 246 claudeSettings = { ...DEFAULT_CLAUDE_SETTINGS, ...stored }; 247 248 // Restore API key to client 249 if (claudeSettings.apiKey) { 250 getClaudeClient().setApiKey(claudeSettings.apiKey); 251 console.log('[Mnemonic] Claude API key restored from storage'); 252 } 253 } 254 } catch (e) { 255 console.error('[Mnemonic] Failed to load Claude settings:', e); 256 } 257 } 258 259 /** 260 * Enrich tabs that have generic titles by fetching page content 261 * Only fetches content for tabs where the title doesn't describe the page 262 */ 263 async function enrichTabsWithContent( 264 tabs: Array<{ url: string; title: string }>, 265 maxTabs: number = 10 266 ): Promise<TabEnrichmentResult> { 267 const result: TabEnrichmentResult = { 268 tabs: [], 269 enrichedCount: 0, 270 errors: [], 271 }; 272 273 // Identify tabs that need enrichment 274 const tabsNeedingEnrichment: Array<{ index: number; url: string; title: string }> = []; 275 276 for (let i = 0; i < tabs.length; i++) { 277 const tab = tabs[i]; 278 if (isGenericTitle(tab.title, tab.url)) { 279 tabsNeedingEnrichment.push({ index: i, ...tab }); 280 } 281 } 282 283 // Limit the number of tabs to fetch (to avoid overloading) 284 const tabsToFetch = tabsNeedingEnrichment.slice(0, maxTabs); 285 286 // Fetch content for tabs with generic titles (in parallel, with concurrency limit) 287 const enrichmentPromises = tabsToFetch.map(async (tab) => { 288 try { 289 // Skip non-http URLs 290 if (!tab.url.startsWith('http://') && !tab.url.startsWith('https://')) { 291 return { index: tab.index, enriched: null }; 292 } 293 294 // Fetch the page with a timeout 295 const controller = new AbortController(); 296 const timeout = setTimeout(() => controller.abort(), 5000); 297 298 const response = await fetch(tab.url, { 299 method: 'GET', 300 signal: controller.signal, 301 headers: { 302 'Accept': 'text/html', 303 'User-Agent': 'Mozilla/5.0 (compatible; Mnemonic/1.0)', 304 }, 305 }); 306 307 clearTimeout(timeout); 308 309 if (!response.ok) { 310 return { index: tab.index, enriched: null }; 311 } 312 313 // Only get first 50KB of HTML (we only need the head and top of body) 314 const reader = response.body?.getReader(); 315 if (!reader) { 316 return { index: tab.index, enriched: null }; 317 } 318 319 let html = ''; 320 const decoder = new TextDecoder(); 321 const maxBytes = 50 * 1024; // 50KB 322 323 try { 324 while (html.length < maxBytes) { 325 const { done, value } = await reader.read(); 326 if (done) break; 327 html += decoder.decode(value, { stream: true }); 328 } 329 } finally { 330 // Always cancel the reader to prevent FD leaks 331 await reader.cancel().catch(() => { 332 // Ignore cancel errors 333 }); 334 } 335 336 // Extract content from HTML 337 const extracted = extractPageContent(html); 338 const enhancedDescription = buildEnhancedDescription(tab.title, extracted); 339 340 return { 341 index: tab.index, 342 enriched: { 343 heading: extracted.heading, 344 description: enhancedDescription, 345 }, 346 }; 347 } catch (e) { 348 // Log but don't fail the whole operation 349 console.warn(`[Mnemonic] Failed to enrich tab ${tab.url}:`, e); 350 result.errors.push(`${tab.url}: ${e instanceof Error ? e.message : 'Unknown error'}`); 351 return { index: tab.index, enriched: null }; 352 } 353 }); 354 355 const enrichmentResults = await Promise.all(enrichmentPromises); 356 357 // Build the result array with all tabs 358 for (let i = 0; i < tabs.length; i++) { 359 const originalTab = tabs[i]; 360 const enrichment = enrichmentResults.find((r) => r.index === i); 361 362 if (enrichment?.enriched) { 363 result.tabs.push({ 364 url: originalTab.url, 365 title: originalTab.title, 366 enrichedTitle: enrichment.enriched.heading || undefined, 367 description: enrichment.enriched.description, 368 wasEnriched: true, 369 }); 370 result.enrichedCount++; 371 } else { 372 result.tabs.push({ 373 url: originalTab.url, 374 title: originalTab.title, 375 wasEnriched: false, 376 }); 377 } 378 } 379 380 return result; 381 } 382 383 /** 384 * Debounced save - waits for 5 seconds of inactivity before saving 385 */ 386 function debouncedSaveSearchIndex(): void { 387 if (saveIndexTimer) { 388 clearTimeout(saveIndexTimer); 389 } 390 saveIndexTimer = setTimeout(() => { 391 saveSearchIndex(); 392 saveIndexTimer = null; 393 }, 5000); 394 } 395 396 /** 397 * Auto-save tabs for a tracked window (debounced) 398 */ 399 async function autoSaveWindowTabs(windowId: number): Promise<void> { 400 const debugLog = getDebugLogger(); 401 402 // Block auto-save until initial external sync is complete 403 // This prevents overwriting external data before we've pulled it 404 if (!initialSyncComplete) { 405 console.log('[Mnemonic] Auto-save blocked - waiting for initial sync to complete'); 406 debugLog.log('auto_save_skipped', { windowId, reason: 'initial_sync_pending' }); 407 return; 408 } 409 410 const tracker = getWindowTracker(); 411 const mapping = tracker.getWindowMapping(windowId); 412 413 if (!mapping) return; 414 415 debugLog.log('auto_save_triggered', { windowId, workspaceId: mapping.workspaceId }); 416 417 // Clear existing timer for this window 418 const existingTimer = autoSaveTimers.get(windowId); 419 if (existingTimer) { 420 clearTimeout(existingTimer); 421 } 422 423 // Set new debounced timer 424 const timer = setTimeout(async () => { 425 autoSaveTimers.delete(windowId); 426 427 try { 428 // Verify window still exists before capturing tabs 429 try { 430 await browser.windows.get(windowId); 431 } catch { 432 console.log(`[Mnemonic] Auto-save skipped - window ${windowId} no longer exists`); 433 debugLog.log('auto_save_skipped', { windowId, workspaceId: mapping.workspaceId, reason: 'window_gone' }); 434 return; 435 } 436 437 // Check if workspace is a tab workspace (child or standalone) 438 const workspace = await workspaceManager.getById(mapping.workspaceId); 439 if (!workspace || !isTabWorkspace(workspace)) { 440 // Parent workspaces don't have tabs, skip auto-save 441 return; 442 } 443 444 // Capture current tabs 445 const tabs = await captureWindowTabs(windowId); 446 447 // Update workspace 448 await workspaceManager.updateTabs(mapping.workspaceId, tabs); 449 450 console.log(`[Mnemonic] Auto-saved ${tabs.length} tabs for workspace ${mapping.workspaceId}`); 451 debugLog.log('auto_save_completed', { windowId, workspaceId: mapping.workspaceId, tabCount: tabs.length }); 452 453 // Update search index 454 updateWorkspaceInIndex(searchIndex, await workspaceManager.getById(mapping.workspaceId) as import('../lib/types').Workspace); 455 debouncedSaveSearchIndex(); 456 457 // Broadcast the change to all UI components 458 browser.runtime.sendMessage({ 459 type: 'TABS_UPDATED', 460 payload: { 461 workspaceId: mapping.workspaceId, 462 windowId, 463 tabCount: tabs.length 464 }, 465 }).catch(() => { 466 // UI might not be listening, that's okay 467 }); 468 } catch (e) { 469 console.error('[Mnemonic] Auto-save failed:', e); 470 } 471 }, AUTO_SAVE_DEBOUNCE_MS); 472 473 autoSaveTimers.set(windowId, timer); 474 } 475 476 /** 477 * Save search index to storage 478 */ 479 async function saveSearchIndex(): Promise<void> { 480 try { 481 const serialized = serializeIndex(searchIndex); 482 await browser.storage.local.set({ 483 [SEARCH_INDEX_STORAGE_KEY]: serialized, 484 }); 485 console.log('[Mnemonic] Search index saved to storage'); 486 } catch (e) { 487 console.error('[Mnemonic] Failed to save search index:', e); 488 } 489 } 490 491 /** 492 * Load search index from storage 493 */ 494 async function loadSearchIndex(): Promise<boolean> { 495 try { 496 const result = await browser.storage.local.get(SEARCH_INDEX_STORAGE_KEY); 497 const serialized = result[SEARCH_INDEX_STORAGE_KEY]; 498 499 if (serialized) { 500 searchIndex = deserializeIndex(serialized); 501 console.log('[Mnemonic] Search index loaded from storage:', searchIndex.stats); 502 return true; 503 } 504 } catch (e) { 505 console.error('[Mnemonic] Failed to load search index:', e); 506 } 507 return false; 508 } 509 510 /** 511 * Check if cached index is still valid 512 */ 513 async function isIndexCacheFresh(workspaces: import('../lib/types').Workspace[]): Promise<boolean> { 514 if (!searchIndex.stats.lastUpdated) return false; 515 516 const indexDate = new Date(searchIndex.stats.lastUpdated); 517 const now = new Date(); 518 const hoursSinceUpdate = (now.getTime() - indexDate.getTime()) / (1000 * 60 * 60); 519 520 // Index is stale if more than 24 hours old 521 if (hoursSinceUpdate > 24) return false; 522 523 // Check if document count roughly matches workspace + tab + resource count 524 let expectedDocs = workspaces.length; 525 for (const ws of workspaces) { 526 if ('tabs' in ws && Array.isArray(ws.tabs)) { 527 expectedDocs += ws.tabs.length; 528 } 529 if ('resources' in ws && Array.isArray(ws.resources)) { 530 expectedDocs += ws.resources.length; 531 } 532 } 533 534 const actualDocs = searchIndex.stats.documentCount; 535 const tolerance = 0.2; // Allow 20% variance 536 537 return Math.abs(actualDocs - expectedDocs) / expectedDocs < tolerance; 538 } 539 540 /** 541 * Build context menus for "Add to Workspace" functionality 542 * Creates hierarchical menus with standalone workspaces and parent contexts 543 */ 544 async function buildContextMenus(): Promise<void> { 545 try { 546 // Remove all existing menus first (clean slate) 547 await browser.contextMenus.removeAll(); 548 549 const workspaces = await workspaceManager.getAll(); 550 551 // Separate workspaces by type 552 const standaloneWorkspaces = workspaces.filter(ws => ws.type === 'standalone' && !ws.archivedAt); 553 const parentWorkspaces = workspaces.filter(ws => ws.type === 'parent' && !ws.archivedAt); 554 const childWorkspaces = workspaces.filter(ws => ws.type === 'child' && !ws.archivedAt); 555 556 // Check if we have any workspaces to show 557 const hasStandalone = standaloneWorkspaces.length > 0; 558 const hasParents = parentWorkspaces.length > 0; 559 const hasAnyWorkspaces = hasStandalone || childWorkspaces.length > 0; 560 561 // Create two top-level menus 562 const topLevelMenus = [ 563 { id: 'add-to-workspace', title: 'Add to Workspace' }, 564 { id: 'add-to-workspace-open', title: 'Add to Workspace and Open' }, 565 ]; 566 567 for (const menu of topLevelMenus) { 568 // Create top-level menu 569 browser.contextMenus.create({ 570 id: menu.id, 571 title: menu.title, 572 contexts: ['link'], 573 }); 574 575 if (!hasAnyWorkspaces) { 576 // No workspaces - show disabled item 577 browser.contextMenus.create({ 578 id: `${menu.id}-empty`, 579 title: 'No workspaces available', 580 parentId: menu.id, 581 contexts: ['link'], 582 enabled: false, 583 }); 584 continue; 585 } 586 587 const isOpenVariant = menu.id === 'add-to-workspace-open'; 588 const prefix = isOpenVariant ? 'add-open' : 'add'; 589 590 // Add standalone workspaces section 591 if (hasStandalone) { 592 // Separator/header for standalone section 593 browser.contextMenus.create({ 594 id: `${menu.id}-standalone-header`, 595 title: '── Standalone ──', 596 parentId: menu.id, 597 contexts: ['link'], 598 enabled: false, 599 }); 600 601 // Add each standalone workspace 602 for (const ws of standaloneWorkspaces) { 603 browser.contextMenus.create({ 604 id: `${prefix}-${ws.id}`, 605 title: ws.name, 606 parentId: menu.id, 607 contexts: ['link'], 608 }); 609 } 610 } 611 612 // Add parent contexts section 613 if (hasParents) { 614 // Separator/header for contexts section 615 browser.contextMenus.create({ 616 id: `${menu.id}-contexts-header`, 617 title: '── Contexts ──', 618 parentId: menu.id, 619 contexts: ['link'], 620 enabled: false, 621 }); 622 623 // Add each parent context with its children as submenu 624 for (const parent of parentWorkspaces) { 625 const parentWs = parent as ParentWorkspace; 626 const children = childWorkspaces.filter( 627 (ws) => (ws as ChildWorkspace).parentId === parentWs.id 628 ); 629 630 if (children.length === 0) { 631 // Parent with no children - show as disabled 632 browser.contextMenus.create({ 633 id: `${prefix}-parent-${parentWs.id}`, 634 title: `${parentWs.name} (no workspaces)`, 635 parentId: menu.id, 636 contexts: ['link'], 637 enabled: false, 638 }); 639 } else { 640 // Parent with children - create submenu 641 browser.contextMenus.create({ 642 id: `${prefix}-parent-${parentWs.id}`, 643 title: parentWs.name, 644 parentId: menu.id, 645 contexts: ['link'], 646 }); 647 648 // Add child workspaces under parent 649 for (const child of children) { 650 browser.contextMenus.create({ 651 id: `${prefix}-${child.id}`, 652 title: child.name, 653 parentId: `${prefix}-parent-${parentWs.id}`, 654 contexts: ['link'], 655 }); 656 } 657 } 658 } 659 } 660 } 661 662 console.log('[Mnemonic] Context menus built successfully'); 663 } catch (e) { 664 console.error('[Mnemonic] Failed to build context menus:', e); 665 } 666 } 667 668 /** 669 * Handle context menu clicks for "Add to Workspace" functionality 670 */ 671 async function handleContextMenuClick( 672 info: browser.Menus.OnClickData, 673 tab?: browser.Tabs.Tab 674 ): Promise<void> { 675 const menuItemId = info.menuItemId as string; 676 677 // Skip non-actionable items (headers, parent containers) 678 if ( 679 menuItemId.endsWith('-header') || 680 menuItemId.endsWith('-empty') || 681 menuItemId.includes('-parent-') 682 ) { 683 return; 684 } 685 686 // Parse the menu item ID to extract workspace ID and action 687 // Format: "add-{workspaceId}" or "add-open-{workspaceId}" 688 const isOpenVariant = menuItemId.startsWith('add-open-'); 689 const workspaceId = isOpenVariant 690 ? menuItemId.replace('add-open-', '') 691 : menuItemId.replace('add-', ''); 692 693 if (!workspaceId || !info.linkUrl) { 694 console.warn('[Mnemonic] Context menu click missing workspace ID or link URL'); 695 return; 696 } 697 698 // Skip special URLs 699 const url = info.linkUrl; 700 if ( 701 url.startsWith('javascript:') || 702 url.startsWith('data:') || 703 url.startsWith('about:') || 704 url.startsWith('chrome:') || 705 url.startsWith('moz-extension:') || 706 url.startsWith('chrome-extension:') 707 ) { 708 console.log('[Mnemonic] Skipping special URL:', url); 709 return; 710 } 711 712 try { 713 // Get workspace 714 const workspace = await workspaceManager.getById(workspaceId); 715 if (!workspace) { 716 console.error('[Mnemonic] Workspace not found:', workspaceId); 717 return; 718 } 719 720 // Only add to tab workspaces (child or standalone) 721 if (!isTabWorkspace(workspace)) { 722 console.error('[Mnemonic] Cannot add link to parent workspace'); 723 return; 724 } 725 726 // Get link text from the selection or use URL as fallback title 727 // Note: info.selectionText gives selected text, info.linkText is the actual link text 728 const title = (info as { linkText?: string }).linkText || url; 729 730 // Create new tab entry 731 const newTab = { 732 url, 733 title, 734 pinned: false, 735 }; 736 737 // Add to workspace tabs 738 const tabWs = workspace as ChildWorkspace | StandaloneWorkspace; 739 const updatedTabs = [...tabWs.tabs, newTab]; 740 await workspaceManager.updateTabs(workspaceId, updatedTabs); 741 742 console.log(`[Mnemonic] Added link to workspace "${workspace.name}":`, url); 743 744 // Update search index 745 const updatedWorkspace = await workspaceManager.getById(workspaceId); 746 if (updatedWorkspace) { 747 updateWorkspaceInIndex(searchIndex, updatedWorkspace); 748 debouncedSaveSearchIndex(); 749 } 750 751 // Broadcast change to UI 752 broadcastWorkspaceStatusChange(); 753 754 // If "open" variant, focus/open the workspace window 755 if (isOpenVariant) { 756 const settings = await storage.getSettings(); 757 await quickSwitchToWorkspace(workspace, { 758 lazyLoad: settings.lazyLoadTabs, 759 }); 760 } 761 } catch (e) { 762 console.error('[Mnemonic] Failed to add link to workspace:', e); 763 } 764 } 765 766 /** 767 * Find a tab matching the refocus tab settings (URL + position) 768 */ 769 function findRefocusTab( 770 windowTabs: browser.Tabs.Tab[], 771 refocusTab: { url: string; index: number }, 772 dashboardUrl: string 773 ): browser.Tabs.Tab | undefined { 774 // Filter tabs matching the URL (excluding dashboard tabs) 775 const matchingTabs = windowTabs.filter( 776 t => t.url === refocusTab.url && !t.url?.startsWith(dashboardUrl) 777 ); 778 779 if (matchingTabs.length === 0) { 780 return undefined; 781 } 782 783 if (matchingTabs.length === 1) { 784 // Only one match - use it 785 return matchingTabs[0]; 786 } 787 788 // Multiple matches - prefer the one closest to stored index 789 const adjustedIndex = refocusTab.index; 790 return matchingTabs.reduce((closest, tab) => { 791 const tabIndex = tab.index; 792 const closestIndex = closest?.index ?? Infinity; 793 return Math.abs(tabIndex - adjustedIndex) < Math.abs(closestIndex - adjustedIndex) 794 ? tab 795 : closest; 796 }, matchingTabs[0]); 797 } 798 799 export default defineBackground(() => { 800 console.log('[Mnemonic] Background service worker initialized'); 801 802 // Initialize on startup 803 initializeExtension(); 804 805 // Handle extension lifecycle 806 browser.runtime.onInstalled.addListener(async (details) => { 807 if (details.reason === 'install') { 808 console.log('[Mnemonic] First install - initializing storage'); 809 await storage.initialize(); 810 } else if (details.reason === 'update') { 811 console.log('[Mnemonic] Extension updated from', details.previousVersion); 812 await handleUpdate(); 813 } 814 }); 815 816 // Handle messages from popup/dashboard 817 browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { 818 handleMessage(message, sendResponse); 819 return true; // Indicates async response 820 }); 821 822 // Track window lifecycle 823 const tracker = getWindowTracker(); 824 825 browser.windows.onCreated.addListener(async (window) => { 826 console.log('[Mnemonic] Window created:', window.id); 827 if (window.id) { 828 // Check if this window has a dashboard tab 829 const dashboardTab = await findDashboardTabInWindow(window.id); 830 if (dashboardTab) { 831 const workspace = await workspaceManager.getById(dashboardTab.params.workspaceId); 832 if (workspace) { 833 tracker.setMapping({ 834 windowId: window.id, 835 workspaceId: workspace.id, 836 isMainWindow: workspace.type === 'parent', 837 isTranscendent: workspace.isTranscendent, 838 }); 839 } 840 } 841 } 842 }); 843 844 browser.windows.onRemoved.addListener((windowId) => { 845 console.log('[Mnemonic] Window removed:', windowId); 846 // Cancel pending auto-save to prevent saving 0 tabs after window closes 847 const pendingTimer = autoSaveTimers.get(windowId); 848 if (pendingTimer) { 849 clearTimeout(pendingTimer); 850 autoSaveTimers.delete(windowId); 851 console.log(`[Mnemonic] Cancelled pending auto-save timer for closing window ${windowId}`); 852 getDebugLogger().log('auto_save_cancelled', { windowId, reason: 'window_removed' }); 853 } 854 getDebugLogger().log('window_removed', { windowId }); 855 tracker.removeWindow(windowId); 856 }); 857 858 browser.windows.onFocusChanged.addListener(async (windowId) => { 859 if (windowId === browser.windows.WINDOW_ID_NONE) return; 860 861 // Update last accessed time for the workspace 862 const mapping = tracker.getWindowMapping(windowId); 863 if (mapping) { 864 try { 865 const workspace = await workspaceManager.getById(mapping.workspaceId); 866 if (workspace) { 867 workspace.metadata.lastAccessed = new Date().toISOString(); 868 // Note: This triggers a storage write in a real implementation 869 870 // Handle refocus logic - automatically activate designated tab or playing tab when window gains focus 871 if (isTabWorkspace(workspace)) { 872 try { 873 // Get all tabs in this window 874 const windowTabs = await browser.tabs.query({ windowId }); 875 const dashboardUrl = browser.runtime.getURL('workspace-dashboard.html'); 876 877 let targetTab: browser.Tabs.Tab | undefined; 878 let focusReason = ''; 879 880 // Check for playing tab if autoFocusPlayingTab is enabled 881 const playingTab = workspace.autoFocusPlayingTab 882 ? windowTabs.find(t => t.audible && !t.url?.startsWith(dashboardUrl)) 883 : undefined; 884 885 // Determine which tab to focus based on settings 886 if (workspace.refocusTab && workspace.refocusTabOverridesPlaying) { 887 // Refocus tab takes priority over playing tab 888 targetTab = findRefocusTab(windowTabs, workspace.refocusTab, dashboardUrl); 889 focusReason = targetTab ? 'refocus tab (priority)' : ''; 890 } else if (playingTab) { 891 // Playing tab takes priority (autoFocusPlayingTab is enabled) 892 targetTab = playingTab; 893 focusReason = 'currently playing'; 894 } else if (workspace.refocusTab) { 895 // Fall back to refocus tab if no playing tab 896 targetTab = findRefocusTab(windowTabs, workspace.refocusTab, dashboardUrl); 897 focusReason = targetTab ? 'refocus tab' : ''; 898 } 899 900 if (targetTab?.id) { 901 await browser.tabs.update(targetTab.id, { active: true }); 902 console.log(`[Mnemonic] Focused ${focusReason} "${targetTab.title}" in workspace "${workspace.name}"`); 903 } 904 } catch (refocusError) { 905 // Silently fail - refocus is a convenience feature 906 console.warn('[Mnemonic] Refocus/playing tab focus failed:', refocusError); 907 } 908 } 909 } 910 } catch (e) { 911 // Ignore errors during focus change 912 } 913 } 914 }); 915 916 // Track tab changes for auto-save and audible state 917 browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { 918 // Broadcast audible state changes for real-time UI updates (no polling needed) 919 if (changeInfo.audible !== undefined) { 920 broadcastAudibleStateChange(); 921 } 922 923 // Trigger auto-save when tab properties that we track change 924 // This includes: url, title, pinned state, and groupId (Chrome tab groups) 925 const hasRelevantChange = changeInfo.url || 926 changeInfo.title || 927 changeInfo.pinned !== undefined || 928 changeInfo.groupId !== undefined; 929 if (hasRelevantChange && tab.windowId) { 930 const mapping = tracker.getWindowMapping(tab.windowId); 931 if (mapping) { 932 // Skip dashboard tabs 933 const dashboardUrl = browser.runtime.getURL('workspace-dashboard.html'); 934 if (tab.url?.startsWith(dashboardUrl)) return; 935 936 await autoSaveWindowTabs(tab.windowId); 937 } 938 } 939 }); 940 941 // Track tab creation for auto-save 942 browser.tabs.onCreated.addListener(async (tab) => { 943 if (tab.windowId) { 944 const mapping = tracker.getWindowMapping(tab.windowId); 945 if (mapping) { 946 // Skip dashboard tabs 947 const dashboardUrl = browser.runtime.getURL('workspace-dashboard.html'); 948 if (tab.url?.startsWith(dashboardUrl) || tab.pendingUrl?.startsWith(dashboardUrl)) return; 949 950 await autoSaveWindowTabs(tab.windowId); 951 } 952 } 953 }); 954 955 // Track tab moves between windows 956 browser.tabs.onMoved.addListener(async (_tabId, moveInfo) => { 957 // When a tab is moved, auto-save the window 958 const mapping = tracker.getWindowMapping(moveInfo.windowId); 959 if (mapping) { 960 await autoSaveWindowTabs(moveInfo.windowId); 961 } 962 }); 963 964 // Track tab attachment (moved from another window) 965 browser.tabs.onAttached.addListener(async (_tabId, attachInfo) => { 966 const mapping = tracker.getWindowMapping(attachInfo.newWindowId); 967 if (mapping) { 968 await autoSaveWindowTabs(attachInfo.newWindowId); 969 } 970 }); 971 972 // Track tab detachment (moved to another window) 973 browser.tabs.onDetached.addListener(async (_tabId, detachInfo) => { 974 const mapping = tracker.getWindowMapping(detachInfo.oldWindowId); 975 if (mapping) { 976 await autoSaveWindowTabs(detachInfo.oldWindowId); 977 } 978 }); 979 980 // Handle tab closure - auto-save and check dashboard tab 981 browser.tabs.onRemoved.addListener(async (tabId, removeInfo) => { 982 if (removeInfo.isWindowClosing) return; // Window is closing, handled by onRemoved 983 984 const windowId = removeInfo.windowId; 985 const mapping = tracker.getWindowMapping(windowId); 986 987 if (mapping) { 988 // Check if there's still a dashboard tab in this window 989 const remainingDashboard = await findDashboardTabInWindow(windowId); 990 if (!remainingDashboard) { 991 // Dashboard tab was closed - mark window as untracked 992 const workspace = await workspaceManager.getById(mapping.workspaceId); 993 if (workspace) { 994 const untracked = tracker.markAsUntracked(windowId, workspace.name, 'dashboard_closed'); 995 if (untracked) { 996 console.log('[Mnemonic] Window untracked - dashboard tab closed:', { 997 windowId, 998 workspace: workspace.name, 999 }); 1000 // Send notification to UI about untracked window 1001 browser.runtime.sendMessage({ 1002 type: 'WINDOW_UNTRACKED', 1003 payload: untracked, 1004 }).catch(() => { 1005 // UI might not be listening, that's okay 1006 }); 1007 } 1008 } 1009 } else { 1010 // Dashboard tab still exists, auto-save the remaining tabs 1011 await autoSaveWindowTabs(windowId); 1012 } 1013 } 1014 }); 1015 1016 // Handle context menu clicks for "Add to Workspace" functionality 1017 browser.contextMenus.onClicked.addListener(handleContextMenuClick); 1018 1019 // NOTE: Periodic window scanning has been disabled. 1020 // Window matching is now only performed at startup to avoid unwanted re-detection. 1021 // Users can manually trigger a rescan via the Dashboard menu if needed. 1022 }); 1023 1024 /** 1025 * Scan windows and create dashboard tabs for any fingerprint-matched windows 1026 * This is used both during initialization and periodic scans 1027 */ 1028 async function scanWindowsAndCreateDashboardTabs(): Promise<void> { 1029 const tracker = getWindowTracker(); 1030 const workspaces = await workspaceManager.getAll(); 1031 1032 const fingerprintMatches = await tracker.scanAllWindows(workspaces); 1033 const mappings = tracker.getMappings(); 1034 console.log('[Mnemonic] Window scan complete, found', mappings.length, 'window-workspace associations'); 1035 1036 // Create dashboard tabs for windows matched via fingerprinting (windows without dashboard tabs) 1037 if (fingerprintMatches.length > 0) { 1038 console.log('[Mnemonic] Creating dashboard tabs for', fingerprintMatches.length, 'fingerprint-matched windows'); 1039 const dashboardBaseUrl = browser.runtime.getURL('workspace-dashboard.html'); 1040 1041 for (const match of fingerprintMatches) { 1042 try { 1043 const workspace = match.workspace; 1044 const dashboardUrl = new URL(dashboardBaseUrl); 1045 dashboardUrl.searchParams.set('workspaceId', workspace.id); 1046 dashboardUrl.searchParams.set('type', workspace.type); 1047 dashboardUrl.searchParams.set('transcendent', workspace.isTranscendent.toString()); 1048 1049 if (workspace.type === 'child') { 1050 const childWs = workspace as ChildWorkspace; 1051 dashboardUrl.searchParams.set('parentId', childWs.parentId); 1052 } 1053 1054 // Create pinned dashboard tab in the matched window at index 0 (leftmost) 1055 const newTab = await browser.tabs.create({ 1056 windowId: match.windowId, 1057 url: dashboardUrl.toString(), 1058 pinned: true, 1059 active: false, // Don't focus the tab 1060 index: 0, // Place at the beginning 1061 }); 1062 1063 // Move to index 0 to ensure it's first among pinned tabs 1064 if (newTab.id) { 1065 await browser.tabs.move(newTab.id, { index: 0 }); 1066 } 1067 console.log(`[Mnemonic] Created dashboard tab for workspace "${workspace.name}" in window ${match.windowId}`); 1068 } catch (e) { 1069 console.error(`[Mnemonic] Failed to create dashboard tab for window ${match.windowId}:`, e); 1070 } 1071 } 1072 } 1073 } 1074 1075 /** 1076 * Initialize extension on startup 1077 */ 1078 async function initializeExtension(): Promise<void> { 1079 try { 1080 // Initialize debug logger 1081 await getDebugLogger().load(); 1082 1083 await storage.initialize(); 1084 1085 // Check for and run migrations 1086 const config = await storage.read(); 1087 if (config && needsMigration(config)) { 1088 console.log('[Mnemonic] Running migrations...'); 1089 const migrated = migrateConfig(config); 1090 await storage.write(migrated); 1091 } 1092 1093 // Check and repair hierarchy if needed 1094 if (config) { 1095 const { config: repairedConfig, report } = repairHierarchy(config); 1096 if (report.wasRepaired) { 1097 console.log('[Mnemonic] Repaired hierarchy issues:', report.fixes.length); 1098 await storage.write(repairedConfig); 1099 } 1100 } 1101 1102 // Validate current state 1103 const validation = await workspaceManager.validate(); 1104 if (!validation.valid) { 1105 console.warn('[Mnemonic] Validation errors:', validation.errors); 1106 } 1107 1108 // Build or load search index 1109 const allWorkspaces = await workspaceManager.getAll(); 1110 1111 // Try to load cached index first 1112 const loaded = await loadSearchIndex(); 1113 if (loaded && await isIndexCacheFresh(allWorkspaces)) { 1114 console.log('[Mnemonic] Using cached search index'); 1115 } else { 1116 // Build fresh index 1117 searchIndex = buildIndex(allWorkspaces); 1118 console.log('[Mnemonic] Search index built:', searchIndex.stats); 1119 // Save to storage for next time 1120 await saveSearchIndex(); 1121 } 1122 1123 // Load Claude settings (including API key) 1124 await loadClaudeSettings(); 1125 1126 // Initialize sync manager with saved backend setting 1127 const syncManager = getSyncManager(); 1128 let currentConfig = await storage.read(); 1129 let activeBackend = currentConfig?.settings?.syncBackend || 'none'; 1130 1131 // Auto-detect native bridge if no backend is configured 1132 if (activeBackend === 'none') { 1133 console.log('[Mnemonic] No sync backend configured, checking for native bridge...'); 1134 try { 1135 // Try to connect to native bridge with a short timeout 1136 const { getRestBridge } = await import('../lib/sync/rest-bridge'); 1137 const restBridge = getRestBridge(); 1138 1139 // Wrap health check in a timeout promise 1140 const healthCheck = restBridge.health(); 1141 const timeout = new Promise<null>((_, reject) => 1142 setTimeout(() => reject(new Error('timeout')), 2000) 1143 ); 1144 1145 const healthResult = await Promise.race([healthCheck, timeout]); 1146 if (healthResult && healthResult.status === 'ok') { 1147 console.log('[Mnemonic] Native bridge detected, auto-configuring...'); 1148 activeBackend = 'native'; 1149 // Save the auto-detected backend to settings 1150 await storage.updateSettings({ syncBackend: 'native' }); 1151 currentConfig = await storage.read(); 1152 } 1153 } catch (e) { 1154 console.log('[Mnemonic] Native bridge not available, continuing without sync'); 1155 } 1156 } 1157 1158 if (activeBackend !== 'none') { 1159 console.log('[Mnemonic] Initializing sync manager with backend:', activeBackend); 1160 const initSuccess = await syncManager.initialize(activeBackend); 1161 if (initSuccess) { 1162 console.log('[Mnemonic] Sync manager initialized successfully'); 1163 1164 // IMPORTANT: Scan windows FIRST to establish window-workspace mappings 1165 // This must happen BEFORE external sync so we can open synced tabs in the right windows 1166 if (activeBackend === 'native') { 1167 console.log('[Mnemonic:Init] Scanning windows to establish mappings before sync...'); 1168 try { 1169 await scanWindowsAndCreateDashboardTabs(); 1170 const tracker = getWindowTracker(); 1171 const mappings = tracker.getMappings(); 1172 console.log('[Mnemonic:Init] Window scan complete, mappings found:', mappings.length); 1173 for (const m of mappings) { 1174 console.log(`[Mnemonic:Init] Window ${m.windowId} -> Workspace ${m.workspaceId}`); 1175 } 1176 } catch (e) { 1177 console.error('[Mnemonic:Init] Pre-sync window scan error:', e); 1178 } 1179 } 1180 1181 // IMPORTANT: Pull external changes FIRST before pushing local data 1182 // This ensures we don't overwrite external changes on startup 1183 if (activeBackend === 'native') { 1184 console.log('[Mnemonic] Checking for external changes before initial sync...'); 1185 try { 1186 const hadExternalChanges = await syncManager.checkAndMergeExternal(); 1187 if (hadExternalChanges) { 1188 console.log('[Mnemonic] Applied external changes on startup'); 1189 // Reload config after merge 1190 currentConfig = await storage.read(); 1191 } else { 1192 console.log('[Mnemonic] No external changes detected'); 1193 // Only push local if no external changes (avoid unnecessary writes) 1194 if (currentConfig) { 1195 console.log('[Mnemonic] Performing initial sync to REST bridge...'); 1196 syncManager.syncToExternal(currentConfig); 1197 } 1198 } 1199 } catch (e) { 1200 console.error('[Mnemonic] Error checking external changes on startup:', e); 1201 } 1202 1203 // Mark initial sync as complete - now auto-save and storage sync can proceed 1204 initialSyncComplete = true; 1205 console.log('[Mnemonic:Init] Initial sync complete, auto-save and storage sync enabled'); 1206 1207 // Start polling for cross-browser tab transfers 1208 startTransferPolling(); 1209 } else { 1210 // Non-native backend - enable auto-save immediately 1211 initialSyncComplete = true; 1212 console.log('[Mnemonic:Init] Non-native backend, auto-save enabled'); 1213 } 1214 } else { 1215 console.warn('[Mnemonic] Failed to initialize sync manager'); 1216 // Even if sync fails, enable auto-save so extension works 1217 initialSyncComplete = true; 1218 } 1219 } else { 1220 // No sync backend - enable auto-save immediately 1221 initialSyncComplete = true; 1222 console.log('[Mnemonic:Init] No sync backend, auto-save enabled'); 1223 } 1224 1225 // Restore credentials from vault if API key wasn't loaded from plain storage 1226 // (Vault migration removes plaintext keys, so we need to restore from vault on startup) 1227 if (!claudeSettings.apiKey && activeBackend === 'native') { 1228 try { 1229 const { getCredentialVault } = await import('../lib/crypto'); 1230 const { getCredentialsSync } = await import('../lib/sync/credentials-sync'); 1231 const vault = getCredentialVault(); 1232 vault.setCredentialsSync(getCredentialsSync()); 1233 1234 if (await vault.isUnlocked()) { 1235 const claudeCred = await vault.getCredential('claude'); 1236 if (claudeCred && claudeCred.apiKey) { 1237 getClaudeClient().setApiKey(claudeCred.apiKey); 1238 claudeSettings.apiKey = claudeCred.apiKey; 1239 claudeSettings.enabled = true; 1240 if (claudeCred.autoSuggest !== undefined) { 1241 claudeSettings.autoSuggest = claudeCred.autoSuggest; 1242 } 1243 console.log('[Mnemonic] Claude API key restored from vault'); 1244 } 1245 } 1246 } catch (e) { 1247 console.warn('[Mnemonic] Failed to restore credentials from vault:', e); 1248 } 1249 } 1250 1251 // Set up auto-sync: push changes to REST bridge when storage changes 1252 // Guard against duplicate listener registration (can happen if service worker restarts) 1253 if (storageListenerUnsubscribe) { 1254 storageListenerUnsubscribe(); 1255 storageListenerUnsubscribe = null; 1256 } 1257 storageListenerUnsubscribe = storage.onChanged(async (changes) => { 1258 // Block syncing to external until initial sync is complete 1259 // This prevents overwriting external data before we've pulled it 1260 if (!initialSyncComplete) { 1261 console.log('[Mnemonic] Storage sync blocked - waiting for initial sync to complete'); 1262 return; 1263 } 1264 if (syncManager.getBackendType() === 'native' && changes.newValue) { 1265 console.log('[Mnemonic] Storage changed, syncing to REST bridge...'); 1266 syncManager.syncToExternal(changes.newValue); 1267 } 1268 }); 1269 1270 // Scan all windows on startup to re-associate them with workspaces 1271 // This uses fingerprint matching for windows without dashboard tabs 1272 console.log('[Mnemonic] Scanning windows for workspace matching...'); 1273 await scanWindowsAndCreateDashboardTabs(); 1274 1275 // Build context menus for "Add to Workspace" functionality 1276 await buildContextMenus(); 1277 1278 console.log('[Mnemonic] Extension initialized successfully'); 1279 } catch (error) { 1280 console.error('[Mnemonic] Initialization error:', error); 1281 } 1282 } 1283 1284 /** 1285 * Handle extension update 1286 */ 1287 async function handleUpdate(): Promise<void> { 1288 try { 1289 const config = await storage.read(); 1290 if (config && needsMigration(config)) { 1291 console.log('[Mnemonic] Migrating configuration...'); 1292 const migrated = migrateConfig(config); 1293 await storage.write(migrated); 1294 console.log('[Mnemonic] Migration complete'); 1295 } 1296 } catch (error) { 1297 console.error('[Mnemonic] Update migration error:', error); 1298 } 1299 } 1300 1301 /** 1302 * Handle messages from UI components 1303 */ 1304 async function handleMessage( 1305 message: { type: string; payload?: unknown }, 1306 sendResponse: (response: unknown) => void 1307 ): Promise<void> { 1308 try { 1309 switch (message.type) { 1310 case 'PING': 1311 sendResponse({ status: 'ok', timestamp: Date.now() }); 1312 break; 1313 1314 case 'GET_WORKSPACES': 1315 const workspaces = await workspaceManager.getAll(); 1316 sendResponse(workspaces); 1317 break; 1318 1319 case 'GET_PARENTS': 1320 const parents = await workspaceManager.getParents(); 1321 sendResponse(parents); 1322 break; 1323 1324 case 'GET_CHILDREN': 1325 const { parentId } = message.payload as { parentId: string }; 1326 const children = await workspaceManager.getChildren(parentId); 1327 sendResponse(children); 1328 break; 1329 1330 case 'GET_WORKSPACE': 1331 const { id } = message.payload as { id: string }; 1332 const workspace = await workspaceManager.getById(id); 1333 sendResponse(workspace); 1334 break; 1335 1336 case 'CREATE_PARENT': 1337 const createParentPayload = message.payload as { 1338 name: string; 1339 childIds?: string[]; 1340 isTranscendent?: boolean; 1341 }; 1342 const newParent = await workspaceManager.createParent( 1343 createParentPayload.name, 1344 createParentPayload 1345 ); 1346 // Update search index with new parent 1347 updateWorkspaceInIndex(searchIndex, newParent); 1348 debouncedSaveSearchIndex(); 1349 sendResponse({ success: true, workspace: newParent }); 1350 // Rebuild context menus to include new parent 1351 buildContextMenus(); 1352 break; 1353 1354 case 'CREATE_CHILD': 1355 const createChildPayload = message.payload as { 1356 parentId: string; 1357 name: string; 1358 tabs?: { url: string; title: string; pinned: boolean }[]; 1359 openInContextSwitch?: boolean; 1360 syncEnabled?: boolean; 1361 }; 1362 const newChild = await workspaceManager.createChild( 1363 createChildPayload.parentId, 1364 createChildPayload.name, 1365 createChildPayload.tabs || [], 1366 { openInContextSwitch: createChildPayload.openInContextSwitch, syncEnabled: createChildPayload.syncEnabled } 1367 ); 1368 // Update search index with new child 1369 updateWorkspaceInIndex(searchIndex, newChild); 1370 debouncedSaveSearchIndex(); 1371 sendResponse({ success: true, workspace: newChild }); 1372 // Notify dashboards of workspace change 1373 broadcastWorkspaceStatusChange(); 1374 // Rebuild context menus to include new workspace 1375 buildContextMenus(); 1376 break; 1377 1378 case 'CREATE_STANDALONE': 1379 const createStandalonePayload = message.payload as { 1380 name: string; 1381 tabs?: { url: string; title: string; pinned: boolean }[]; 1382 openInContextSwitch?: boolean; 1383 syncEnabled?: boolean; 1384 }; 1385 const newStandalone = await workspaceManager.createStandalone( 1386 createStandalonePayload.name, 1387 createStandalonePayload.tabs || [], 1388 { openInContextSwitch: createStandalonePayload.openInContextSwitch, syncEnabled: createStandalonePayload.syncEnabled } 1389 ); 1390 // Update search index with new standalone 1391 updateWorkspaceInIndex(searchIndex, newStandalone); 1392 debouncedSaveSearchIndex(); 1393 sendResponse({ success: true, workspace: newStandalone }); 1394 // Notify dashboards of workspace change 1395 broadcastWorkspaceStatusChange(); 1396 // Rebuild context menus to include new workspace 1397 buildContextMenus(); 1398 break; 1399 1400 case 'RENAME_WORKSPACE': 1401 const renamePayload = message.payload as { id: string; name: string }; 1402 await workspaceManager.rename(renamePayload.id, renamePayload.name); 1403 // Update search index with renamed workspace 1404 const renamedWs = await workspaceManager.getById(renamePayload.id); 1405 if (renamedWs) { 1406 updateWorkspaceInIndex(searchIndex, renamedWs); 1407 debouncedSaveSearchIndex(); 1408 } 1409 sendResponse({ success: true }); 1410 // Rebuild context menus to reflect renamed workspace 1411 buildContextMenus(); 1412 break; 1413 1414 case 'UPDATE_WORKSPACE': 1415 const updatePayload = message.payload as { id: string; updates: Record<string, unknown> }; 1416 await workspaceManager.update(updatePayload.id, updatePayload.updates); 1417 // Update search index with modified workspace 1418 const updatedWs = await workspaceManager.getById(updatePayload.id); 1419 if (updatedWs) { 1420 updateWorkspaceInIndex(searchIndex, updatedWs); 1421 debouncedSaveSearchIndex(); 1422 } 1423 sendResponse({ success: true }); 1424 break; 1425 1426 case 'DELETE_WORKSPACE': 1427 const deletePayload = message.payload as { id: string; keepChildren?: boolean }; 1428 await workspaceManager.delete(deletePayload.id, { 1429 keepChildren: deletePayload.keepChildren ?? true, 1430 }); 1431 // Remove from search index 1432 removeWorkspaceFromIndex(searchIndex, deletePayload.id); 1433 debouncedSaveSearchIndex(); 1434 sendResponse({ success: true }); 1435 // Notify dashboards of workspace change 1436 broadcastWorkspaceStatusChange(); 1437 // Rebuild context menus to remove deleted workspace 1438 buildContextMenus(); 1439 break; 1440 1441 case 'MOVE_WORKSPACE': 1442 const movePayload = message.payload as { id: string; newParentId: string | null }; 1443 await workspaceManager.move(movePayload.id, movePayload.newParentId); 1444 // Update search index with moved workspace (parent name may have changed) 1445 const movedWs = await workspaceManager.getById(movePayload.id); 1446 if (movedWs) { 1447 updateWorkspaceInIndex(searchIndex, movedWs); 1448 debouncedSaveSearchIndex(); 1449 } 1450 sendResponse({ success: true }); 1451 // Rebuild context menus to reflect workspace move 1452 buildContextMenus(); 1453 break; 1454 1455 case 'SET_TRANSCENDENT': 1456 const transcendentPayload = message.payload as { id: string; isTranscendent: boolean }; 1457 await workspaceManager.setTranscendent( 1458 transcendentPayload.id, 1459 transcendentPayload.isTranscendent 1460 ); 1461 // Update search index with transcendence change 1462 const transcendentWs = await workspaceManager.getById(transcendentPayload.id); 1463 if (transcendentWs) { 1464 updateWorkspaceInIndex(searchIndex, transcendentWs); 1465 debouncedSaveSearchIndex(); 1466 } 1467 sendResponse({ success: true }); 1468 break; 1469 1470 case 'CONVERT_TO_PARENT': 1471 const convertPayload = message.payload as { id: string }; 1472 await workspaceManager.convertToParent(convertPayload.id); 1473 // Update search index with converted workspace (type changed) 1474 const convertedWs = await workspaceManager.getById(convertPayload.id); 1475 if (convertedWs) { 1476 updateWorkspaceInIndex(searchIndex, convertedWs); 1477 debouncedSaveSearchIndex(); 1478 } 1479 sendResponse({ success: true }); 1480 break; 1481 1482 // ===================================================================== 1483 // Workspace Group Operations 1484 // ===================================================================== 1485 1486 case 'CREATE_GROUP': 1487 try { 1488 const createGroupPayload = message.payload as { 1489 parentId: string; 1490 name: string; 1491 color?: string; 1492 }; 1493 const newGroup = await workspaceManager.createGroup( 1494 createGroupPayload.parentId, 1495 createGroupPayload.name, 1496 { color: createGroupPayload.color } 1497 ); 1498 sendResponse({ success: true, group: newGroup }); 1499 broadcastWorkspaceStatusChange(); 1500 } catch (e) { 1501 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to create group' }); 1502 } 1503 break; 1504 1505 case 'RENAME_GROUP': 1506 try { 1507 const renameGroupPayload = message.payload as { 1508 parentId: string; 1509 groupId: string; 1510 name: string; 1511 }; 1512 await workspaceManager.renameGroup( 1513 renameGroupPayload.parentId, 1514 renameGroupPayload.groupId, 1515 renameGroupPayload.name 1516 ); 1517 sendResponse({ success: true }); 1518 broadcastWorkspaceStatusChange(); 1519 } catch (e) { 1520 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to rename group' }); 1521 } 1522 break; 1523 1524 case 'DELETE_GROUP': 1525 try { 1526 const deleteGroupPayload = message.payload as { 1527 parentId: string; 1528 groupId: string; 1529 }; 1530 await workspaceManager.deleteGroup( 1531 deleteGroupPayload.parentId, 1532 deleteGroupPayload.groupId 1533 ); 1534 sendResponse({ success: true }); 1535 broadcastWorkspaceStatusChange(); 1536 } catch (e) { 1537 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to delete group' }); 1538 } 1539 break; 1540 1541 case 'SET_CHILD_GROUP': 1542 try { 1543 const setGroupPayload = message.payload as { 1544 childId: string; 1545 groupId: string | null; 1546 }; 1547 await workspaceManager.setChildGroup( 1548 setGroupPayload.childId, 1549 setGroupPayload.groupId 1550 ); 1551 sendResponse({ success: true }); 1552 broadcastWorkspaceStatusChange(); 1553 } catch (e) { 1554 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to set child group' }); 1555 } 1556 break; 1557 1558 case 'TOGGLE_GROUP_COLLAPSED': 1559 try { 1560 const togglePayload = message.payload as { 1561 parentId: string; 1562 groupId: string; 1563 }; 1564 await workspaceManager.toggleGroupCollapsed( 1565 togglePayload.parentId, 1566 togglePayload.groupId 1567 ); 1568 sendResponse({ success: true }); 1569 broadcastWorkspaceStatusChange(); 1570 } catch (e) { 1571 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to toggle group' }); 1572 } 1573 break; 1574 1575 case 'UPDATE_GROUP_COLOR': 1576 try { 1577 const colorPayload = message.payload as { 1578 parentId: string; 1579 groupId: string; 1580 color: string | null; 1581 }; 1582 await workspaceManager.updateGroupColor( 1583 colorPayload.parentId, 1584 colorPayload.groupId, 1585 colorPayload.color 1586 ); 1587 sendResponse({ success: true }); 1588 broadcastWorkspaceStatusChange(); 1589 } catch (e) { 1590 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to update group color' }); 1591 } 1592 break; 1593 1594 case 'SET_OPEN_IN_CONTEXT_SWITCH': 1595 const contextSwitchPayload = message.payload as { id: string; openInContextSwitch: boolean }; 1596 await workspaceManager.setOpenInContextSwitch( 1597 contextSwitchPayload.id, 1598 contextSwitchPayload.openInContextSwitch 1599 ); 1600 sendResponse({ success: true }); 1601 break; 1602 1603 case 'SET_SYNC_ENABLED': 1604 const syncEnabledPayload = message.payload as { id: string; syncEnabled: boolean }; 1605 await workspaceManager.setSyncEnabled( 1606 syncEnabledPayload.id, 1607 syncEnabledPayload.syncEnabled 1608 ); 1609 sendResponse({ success: true }); 1610 break; 1611 1612 case 'SET_REFOCUS_TAB': 1613 try { 1614 const setRefocusPayload = message.payload as { 1615 workspaceId: string; 1616 url: string; 1617 index: number; 1618 }; 1619 const wsForRefocus = await workspaceManager.getById(setRefocusPayload.workspaceId); 1620 if (!wsForRefocus || !isTabWorkspace(wsForRefocus)) { 1621 sendResponse({ success: false, error: 'Workspace not found or not a tab workspace' }); 1622 break; 1623 } 1624 await workspaceManager.update(setRefocusPayload.workspaceId, { 1625 refocusTab: { 1626 url: setRefocusPayload.url, 1627 index: setRefocusPayload.index, 1628 }, 1629 }); 1630 console.log(`[Mnemonic] Set refocus tab for workspace "${wsForRefocus.name}":`, setRefocusPayload.url); 1631 sendResponse({ success: true }); 1632 broadcastWorkspaceStatusChange(); 1633 } catch (e) { 1634 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to set refocus tab' }); 1635 } 1636 break; 1637 1638 case 'CLEAR_REFOCUS_TAB': 1639 try { 1640 const clearRefocusPayload = message.payload as { workspaceId: string }; 1641 const wsForClearRefocus = await workspaceManager.getById(clearRefocusPayload.workspaceId); 1642 if (!wsForClearRefocus || !isTabWorkspace(wsForClearRefocus)) { 1643 sendResponse({ success: false, error: 'Workspace not found or not a tab workspace' }); 1644 break; 1645 } 1646 await workspaceManager.update(clearRefocusPayload.workspaceId, { 1647 refocusTab: undefined, 1648 }); 1649 console.log(`[Mnemonic] Cleared refocus tab for workspace "${wsForClearRefocus.name}"`); 1650 sendResponse({ success: true }); 1651 broadcastWorkspaceStatusChange(); 1652 } catch (e) { 1653 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to clear refocus tab' }); 1654 } 1655 break; 1656 1657 case 'SET_AUTO_FOCUS_PLAYING_TAB': 1658 try { 1659 const autoFocusPayload = message.payload as { workspaceId: string; enabled: boolean }; 1660 const wsForAutoFocus = await workspaceManager.getById(autoFocusPayload.workspaceId); 1661 if (!wsForAutoFocus || !isTabWorkspace(wsForAutoFocus)) { 1662 sendResponse({ success: false, error: 'Workspace not found or not a tab workspace' }); 1663 break; 1664 } 1665 await workspaceManager.update(autoFocusPayload.workspaceId, { 1666 autoFocusPlayingTab: autoFocusPayload.enabled, 1667 }); 1668 console.log(`[Mnemonic] Set autoFocusPlayingTab=${autoFocusPayload.enabled} for workspace "${wsForAutoFocus.name}"`); 1669 sendResponse({ success: true }); 1670 broadcastWorkspaceStatusChange(); 1671 } catch (e) { 1672 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to set auto focus playing tab' }); 1673 } 1674 break; 1675 1676 case 'SET_REFOCUS_TAB_OVERRIDES_PLAYING': 1677 try { 1678 const overridesPayload = message.payload as { workspaceId: string; enabled: boolean }; 1679 const wsForOverrides = await workspaceManager.getById(overridesPayload.workspaceId); 1680 if (!wsForOverrides || !isTabWorkspace(wsForOverrides)) { 1681 sendResponse({ success: false, error: 'Workspace not found or not a tab workspace' }); 1682 break; 1683 } 1684 await workspaceManager.update(overridesPayload.workspaceId, { 1685 refocusTabOverridesPlaying: overridesPayload.enabled, 1686 }); 1687 console.log(`[Mnemonic] Set refocusTabOverridesPlaying=${overridesPayload.enabled} for workspace "${wsForOverrides.name}"`); 1688 sendResponse({ success: true }); 1689 broadcastWorkspaceStatusChange(); 1690 } catch (e) { 1691 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to set refocus tab overrides playing' }); 1692 } 1693 break; 1694 1695 case 'UPDATE_TABS': 1696 const updateTabsPayload = message.payload as { 1697 id: string; 1698 tabs: { url: string; title: string; pinned: boolean }[]; 1699 }; 1700 await workspaceManager.updateTabs(updateTabsPayload.id, updateTabsPayload.tabs); 1701 // Update search index with new tabs 1702 const wsAfterTabUpdate = await workspaceManager.getById(updateTabsPayload.id); 1703 if (wsAfterTabUpdate) { 1704 updateWorkspaceInIndex(searchIndex, wsAfterTabUpdate); 1705 debouncedSaveSearchIndex(); 1706 } 1707 sendResponse({ success: true }); 1708 break; 1709 1710 case 'UPDATE_SUMMARY': 1711 const updateSummaryPayload = message.payload as { 1712 id: string; 1713 text: string; 1714 isUserGenerated: boolean; 1715 }; 1716 await workspaceManager.updateSummary( 1717 updateSummaryPayload.id, 1718 updateSummaryPayload.text, 1719 updateSummaryPayload.isUserGenerated 1720 ); 1721 // Update search index with new summary 1722 const wsAfterSummaryUpdate = await workspaceManager.getById(updateSummaryPayload.id); 1723 if (wsAfterSummaryUpdate) { 1724 updateWorkspaceInIndex(searchIndex, wsAfterSummaryUpdate); 1725 debouncedSaveSearchIndex(); 1726 } 1727 sendResponse({ success: true }); 1728 break; 1729 1730 case 'ENRICH_TABS': 1731 // Fetch page content for tabs with generic titles 1732 const enrichTabsPayload = message.payload as { 1733 tabs: Array<{ url: string; title: string }>; 1734 maxTabs?: number; 1735 }; 1736 try { 1737 const enrichResult = await enrichTabsWithContent( 1738 enrichTabsPayload.tabs, 1739 enrichTabsPayload.maxTabs || 10 1740 ); 1741 sendResponse({ success: true, result: enrichResult }); 1742 } catch (e) { 1743 sendResponse({ 1744 success: false, 1745 error: e instanceof Error ? e.message : 'Failed to enrich tabs', 1746 }); 1747 } 1748 break; 1749 1750 case 'GENERATE_WORKSPACE_SUMMARY': 1751 // Auto-generate summary for a workspace (used after workspace creation) 1752 const genSummaryPayload = message.payload as { workspaceId: string }; 1753 try { 1754 // Check if Claude is configured 1755 if (!claudeSettings.enabled || !claudeSettings.apiKey) { 1756 sendResponse({ success: false, error: 'Claude not configured' }); 1757 break; 1758 } 1759 1760 // Get the workspace 1761 const wsForSummary = await workspaceManager.getById(genSummaryPayload.workspaceId); 1762 if (!wsForSummary) { 1763 sendResponse({ success: false, error: 'Workspace not found' }); 1764 break; 1765 } 1766 1767 // Only generate for tab workspaces (child/standalone) 1768 if (!isTabWorkspace(wsForSummary)) { 1769 sendResponse({ success: false, error: 'Cannot generate summary for parent workspace' }); 1770 break; 1771 } 1772 1773 // Ensure Claude client is configured 1774 const claudeClient = getClaudeClient(); 1775 claudeClient.setApiKey(claudeSettings.apiKey); 1776 if (claudeSettings.model) { 1777 claudeClient.setModel(claudeSettings.model); 1778 } 1779 1780 // Enrich tabs with content for better context 1781 const enrichedResult = await enrichTabsWithContent(wsForSummary.tabs, 15); 1782 1783 // Generate summary 1784 const suggestionEngine = getSuggestionEngine(); 1785 const summaryResult = await suggestionEngine.generateSummaryFromTabs( 1786 wsForSummary.name, 1787 enrichedResult.tabs.map(t => ({ 1788 url: t.url, 1789 title: t.title, 1790 enrichedTitle: t.enrichedTitle, 1791 description: t.description, 1792 })) 1793 ); 1794 1795 if (summaryResult.success && summaryResult.summary) { 1796 // Save the summary 1797 await workspaceManager.updateSummary( 1798 genSummaryPayload.workspaceId, 1799 summaryResult.summary, 1800 false // Not user generated 1801 ); 1802 sendResponse({ success: true, summary: summaryResult.summary }); 1803 } else { 1804 sendResponse({ success: false, error: summaryResult.error || 'Failed to generate summary' }); 1805 } 1806 } catch (e) { 1807 sendResponse({ 1808 success: false, 1809 error: e instanceof Error ? e.message : 'Failed to generate summary', 1810 }); 1811 } 1812 break; 1813 1814 case 'EXPORT_CONFIG': 1815 const exported = await storage.export(); 1816 sendResponse({ success: true, data: exported }); 1817 break; 1818 1819 case 'IMPORT_CONFIG': 1820 const importPayload = message.payload as { data: string }; 1821 const importResult = await storage.import(importPayload.data); 1822 sendResponse({ success: importResult.success, error: importResult.error }); 1823 break; 1824 1825 case 'VALIDATE': 1826 const validationResult = await workspaceManager.validate(); 1827 sendResponse(validationResult); 1828 break; 1829 1830 case 'GET_SETTINGS': 1831 const settings = await storage.getSettings(); 1832 sendResponse(settings); 1833 break; 1834 1835 case 'UPDATE_SETTINGS': 1836 const settingsPayload = message.payload as Record<string, unknown>; 1837 await storage.updateSettings(settingsPayload); 1838 sendResponse({ success: true }); 1839 break; 1840 1841 case 'INIT_SYNC_BACKEND': 1842 try { 1843 const initBackendPayload = message.payload as { backendType: 'none' | 'native' | 'filesystem' }; 1844 console.log('[Mnemonic] INIT_SYNC_BACKEND called with backend:', initBackendPayload.backendType); 1845 const initBackendManager = getSyncManager(); 1846 const initialized = await initBackendManager.initialize(initBackendPayload.backendType); 1847 console.log('[Mnemonic] Sync initialize result:', initialized); 1848 1849 // When switching to 'native', auto-pull from external if local is empty 1850 if (initialized && initBackendPayload.backendType === 'native') { 1851 const currentWorkspaces = await workspaceManager.getAll(); 1852 console.log('[Mnemonic] Current local workspaces count:', currentWorkspaces.length); 1853 if (currentWorkspaces.length === 0) { 1854 console.log('[Mnemonic] Native sync enabled with empty local storage, pulling from external...'); 1855 try { 1856 const pullResult = await initBackendManager.forcePullFromExternal(); 1857 console.log('[Mnemonic] Pulled existing sync data, result:', pullResult); 1858 } catch (error) { 1859 console.log('[Mnemonic] No existing sync data or pull failed:', error); 1860 } 1861 } 1862 1863 // After sync backend is initialized, scan windows and create dashboard tabs 1864 console.log('[Mnemonic] Sync backend initialized, scanning windows...'); 1865 setTimeout(async () => { 1866 try { 1867 await scanWindowsAndCreateDashboardTabs(); 1868 } catch (e) { 1869 console.error('[Mnemonic] Post-sync window scan error:', e); 1870 } 1871 }, 1000); // Small delay to ensure data is fully synced 1872 } 1873 1874 sendResponse({ success: initialized }); 1875 } catch (e) { 1876 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to init sync' }); 1877 } 1878 break; 1879 1880 case 'SELECT_SYNC_DIRECTORY': 1881 try { 1882 const selectDirManager = getSyncManager(); 1883 const selected = await selectDirManager.selectDirectory(); 1884 sendResponse({ success: selected }); 1885 } catch (e) { 1886 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to select directory' }); 1887 } 1888 break; 1889 1890 // Window management messages 1891 case 'SWITCH_CONTEXT': 1892 const switchPayload = message.payload as ContextSwitchRequest; 1893 const allWorkspaces = await workspaceManager.getAll(); 1894 const switchSettings = await storage.getSettings(); 1895 const switchResult = await switchContext(switchPayload, allWorkspaces, { 1896 lazyLoad: switchSettings.lazyLoadTabs, 1897 }); 1898 sendResponse(switchResult); 1899 // Notify dashboards of status change after context switch 1900 broadcastWorkspaceStatusChange(); 1901 break; 1902 1903 case 'QUICK_SWITCH': 1904 const quickSwitchPayload = message.payload as { workspaceId: string }; 1905 const targetWorkspace = await workspaceManager.getById(quickSwitchPayload.workspaceId); 1906 if (targetWorkspace) { 1907 const quickSwitchSettings = await storage.getSettings(); 1908 const windowId = await quickSwitchToWorkspace(targetWorkspace, { 1909 lazyLoad: quickSwitchSettings.lazyLoadTabs, 1910 }); 1911 sendResponse({ success: windowId !== null, windowId }); 1912 // Notify dashboards of status change 1913 broadcastWorkspaceStatusChange(); 1914 } else { 1915 sendResponse({ success: false, error: 'Workspace not found' }); 1916 } 1917 break; 1918 1919 case 'CLOSE_WORKSPACE_WINDOWS': 1920 const closePayload = message.payload as { workspaceId: string }; 1921 const closed = await closeWorkspaceWindows(closePayload.workspaceId); 1922 sendResponse({ success: closed }); 1923 // Notify dashboards of status change 1924 broadcastWorkspaceStatusChange(); 1925 break; 1926 1927 case 'SAVE_WINDOW_STATE': 1928 const saveStatePayload = message.payload as { workspaceId: string; windowId: number }; 1929 const wsToSave = await workspaceManager.getById(saveStatePayload.workspaceId); 1930 if (wsToSave && isTabWorkspace(wsToSave)) { 1931 const tabs = await captureWindowTabs(saveStatePayload.windowId); 1932 const position = await captureWindowPosition(saveStatePayload.windowId); 1933 const updatedWs = await refreshWorkspaceFingerprint(wsToSave as TabWorkspace, saveStatePayload.windowId); 1934 await workspaceManager.updateTabs(saveStatePayload.workspaceId, tabs); 1935 if (position) { 1936 // Update window position in storage 1937 updatedWs.windowPosition = position; 1938 } 1939 sendResponse({ success: true, tabs: tabs.length, position }); 1940 } else { 1941 sendResponse({ success: false, error: 'Workspace not found or not a tab workspace' }); 1942 } 1943 break; 1944 1945 case 'GET_WINDOW_MAPPINGS': 1946 const tracker = getWindowTracker(); 1947 sendResponse(tracker.getMappings()); 1948 break; 1949 1950 case 'IDENTIFY_WINDOW': 1951 const identifyPayload = message.payload as { windowId: number }; 1952 const windowTracker = getWindowTracker(); 1953 const wsForIdentify = await workspaceManager.getAll(); 1954 const identification = await windowTracker.identifyWindow(identifyPayload.windowId, wsForIdentify); 1955 sendResponse(identification); 1956 break; 1957 1958 case 'SCAN_ALL_WINDOWS': 1959 const scanTracker = getWindowTracker(); 1960 const wsForScan = await workspaceManager.getAll(); 1961 await scanTracker.scanAllWindows(wsForScan); 1962 sendResponse({ success: true, mappings: scanTracker.getMappings() }); 1963 break; 1964 1965 // Search messages 1966 case 'SEARCH': 1967 const searchPayload = message.payload as { query: string; options?: SearchOptions; generateMissingSummaries?: boolean }; 1968 let searchResults = search(searchIndex, searchPayload.query, searchPayload.options); 1969 1970 // If requested, generate summaries for workspaces missing them (max 3 to avoid delays) 1971 if (searchPayload.generateMissingSummaries && claudeSettings.enabled && claudeSettings.apiKey) { 1972 const workspaceResultsWithoutSummary = searchResults.results 1973 .filter(r => r.document.type === 'workspace' && !r.document.summary) 1974 .slice(0, 3); 1975 1976 if (workspaceResultsWithoutSummary.length > 0) { 1977 const suggestionEngine = getSuggestionEngine(); 1978 1979 for (const result of workspaceResultsWithoutSummary) { 1980 try { 1981 const ws = await workspaceManager.getById(result.document.id); 1982 if (!ws || ws.summary) continue; // Skip if not found or already has summary 1983 1984 // Generate summary based on workspace type 1985 let generatedSummary: string | undefined; 1986 1987 if (ws.type === 'parent') { 1988 // For parent workspaces, summarize from children 1989 const allWs = await workspaceManager.getAll(); 1990 const children = allWs.filter(w => w.type === 'child' && (w as { parentId: string }).parentId === ws.id); 1991 const childrenWithSummaries = children.map(c => ({ 1992 name: c.name, 1993 summary: (c as { summary?: { text?: string } }).summary?.text 1994 })); 1995 const parentResult = await suggestionEngine.generateSummaryFromChildren(ws.name, childrenWithSummaries); 1996 generatedSummary = parentResult?.summary; 1997 } else { 1998 // For tab workspaces, summarize from tabs 1999 const tabWs = ws as { tabs?: { url: string; title: string }[] }; 2000 if (tabWs.tabs && tabWs.tabs.length > 0) { 2001 const enrichedTabs = await enrichTabsWithContent(tabWs.tabs, 10); 2002 const tabResult = await suggestionEngine.generateSummaryFromTabs(ws.name, enrichedTabs); 2003 generatedSummary = tabResult?.summary; 2004 } 2005 } 2006 2007 // Save the generated summary 2008 if (generatedSummary) { 2009 await workspaceManager.updateSummary(ws.id, generatedSummary, false); 2010 // Update the search result document with the new summary 2011 result.document.summary = generatedSummary; 2012 // Re-index the workspace 2013 const updatedWs = await workspaceManager.getById(ws.id); 2014 if (updatedWs) { 2015 updateWorkspaceInIndex(searchIndex, updatedWs); 2016 } 2017 } 2018 } catch (e) { 2019 console.warn(`Failed to generate summary for workspace ${result.document.id}:`, e); 2020 } 2021 } 2022 } 2023 } 2024 2025 sendResponse(searchResults); 2026 break; 2027 2028 case 'GET_SEARCH_SUGGESTIONS': 2029 const suggestPayload = message.payload as { prefix: string; limit?: number }; 2030 const searchSuggestionResults = getSearchSuggestions(searchIndex, suggestPayload.prefix, suggestPayload.limit); 2031 sendResponse(searchSuggestionResults); 2032 break; 2033 2034 case 'QUICK_SEARCH': 2035 const quickSearchPayload = message.payload as { query: string; limit?: number }; 2036 const quickResults = quickSearch(searchIndex, quickSearchPayload.query, quickSearchPayload.limit); 2037 sendResponse(quickResults); 2038 break; 2039 2040 case 'REBUILD_SEARCH_INDEX': 2041 const wsForIndex = await workspaceManager.getAll(); 2042 searchIndex = buildIndex(wsForIndex); 2043 console.log('[Mnemonic] Search index rebuilt:', searchIndex.stats); 2044 // Persist the rebuilt index 2045 await saveSearchIndex(); 2046 sendResponse({ success: true, stats: searchIndex.stats }); 2047 break; 2048 2049 case 'UPDATE_SEARCH_INDEX': 2050 const updateIndexPayload = message.payload as { workspaceId: string }; 2051 const wsToIndex = await workspaceManager.getById(updateIndexPayload.workspaceId); 2052 if (wsToIndex) { 2053 updateWorkspaceInIndex(searchIndex, wsToIndex); 2054 sendResponse({ success: true }); 2055 } else { 2056 removeWorkspaceFromIndex(searchIndex, updateIndexPayload.workspaceId); 2057 sendResponse({ success: true, removed: true }); 2058 } 2059 // Schedule debounced save 2060 debouncedSaveSearchIndex(); 2061 break; 2062 2063 // Claude API messages 2064 case 'SET_CLAUDE_API_KEY': 2065 const apiKeyPayload = message.payload as { apiKey: string | null }; 2066 const claudeClient = getClaudeClient(); 2067 claudeClient.setApiKey(apiKeyPayload.apiKey); 2068 claudeSettings.apiKey = apiKeyPayload.apiKey; 2069 claudeSettings.enabled = apiKeyPayload.apiKey !== null; 2070 if (!apiKeyPayload.apiKey) { 2071 setClaudeErrorBadge(false); 2072 } 2073 // Persist to storage 2074 saveClaudeSettings(); 2075 sendResponse({ success: true, enabled: claudeSettings.enabled }); 2076 break; 2077 2078 case 'GET_CLAUDE_SETTINGS': 2079 sendResponse({ 2080 ...claudeSettings, 2081 apiKey: claudeSettings.apiKey ? '****' : null, // Don't send actual key 2082 isConfigured: getClaudeClient().isConfigured(), 2083 }); 2084 break; 2085 2086 case 'UPDATE_CLAUDE_SETTINGS': 2087 const claudeSettingsPayload = message.payload as Partial<ClaudeSettings>; 2088 claudeSettings = { ...claudeSettings, ...claudeSettingsPayload }; 2089 if (claudeSettingsPayload.model) { 2090 getClaudeClient().setModel(claudeSettingsPayload.model); 2091 } 2092 // Persist to storage 2093 saveClaudeSettings(); 2094 sendResponse({ success: true }); 2095 break; 2096 2097 case 'TEST_CLAUDE_CONNECTION': 2098 const testClient = getClaudeClient(); 2099 if (!testClient.isConfigured()) { 2100 sendResponse({ success: false, error: 'API key not configured' }); 2101 } else { 2102 const testResult = await testClient.testConnection(); 2103 setClaudeErrorBadge(!testResult.success); 2104 sendResponse(testResult); 2105 } 2106 break; 2107 2108 case 'GET_WORKSPACE_SUGGESTIONS': 2109 const suggestionEngine = getSuggestionEngine(); 2110 const wsForSuggestions = await workspaceManager.getAll(); 2111 const suggestionResult = await suggestionEngine.getSuggestions(wsForSuggestions); 2112 setClaudeErrorBadge(!suggestionResult.success); 2113 sendResponse(suggestionResult); 2114 break; 2115 2116 case 'SUGGEST_PARENT_NAME': 2117 const parentNamePayload = message.payload as { childWorkspaceIds: string[] }; 2118 const childWorkspacesForName = await Promise.all( 2119 parentNamePayload.childWorkspaceIds.map(id => workspaceManager.getById(id)) 2120 ); 2121 const validChildren = childWorkspacesForName.filter( 2122 (ws): ws is ChildWorkspace => ws !== null && ws.type === 'child' 2123 ); 2124 const nameSuggestions = await getSuggestionEngine().suggestParentName(validChildren); 2125 sendResponse({ success: true, suggestions: nameSuggestions }); 2126 break; 2127 2128 case 'CATEGORIZE_TABS': 2129 const categorizePayload = message.payload as { tabs: { url: string; title: string; pinned: boolean }[] }; 2130 const existingWs = await workspaceManager.getAll(); 2131 const categorization = await getSuggestionEngine().categorizeOrphanTabs( 2132 categorizePayload.tabs, 2133 existingWs 2134 ); 2135 setClaudeErrorBadge(!categorization.success); 2136 sendResponse(categorization); 2137 break; 2138 2139 case 'SUGGEST_TRANSCENDENT': 2140 try { 2141 const wsForTranscendent = await workspaceManager.getAll(); 2142 const transcendentIds = await getSuggestionEngine().suggestTranscendent(wsForTranscendent); 2143 setClaudeErrorBadge(false); 2144 sendResponse({ success: true, workspaceIds: transcendentIds }); 2145 } catch (e) { 2146 setClaudeErrorBadge(true); 2147 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to get suggestions' }); 2148 } 2149 break; 2150 2151 // Untracked window management 2152 case 'GET_UNTRACKED_WINDOWS': 2153 sendResponse({ windows: getWindowTracker().getUntrackedWindows() }); 2154 break; 2155 2156 case 'RETRACK_WINDOW': 2157 const retrackPayload = message.payload as { windowId: number; workspaceId: string }; 2158 const wsToRetrack = await workspaceManager.getById(retrackPayload.workspaceId); 2159 if (wsToRetrack) { 2160 getWindowTracker().retrackWindow( 2161 retrackPayload.windowId, 2162 retrackPayload.workspaceId, 2163 wsToRetrack.isTranscendent 2164 ); 2165 sendResponse({ success: true }); 2166 } else { 2167 sendResponse({ success: false, error: 'Workspace not found' }); 2168 } 2169 break; 2170 2171 case 'CLEAR_UNTRACKED': 2172 const clearPayload = message.payload as { windowId: number }; 2173 getWindowTracker().clearUntracked(clearPayload.windowId); 2174 sendResponse({ success: true }); 2175 break; 2176 2177 case 'GET_CONTEXT_SWITCH_CONFIRMATION': 2178 const confirmPayload = message.payload as { targetParentId: string }; 2179 const allWsForConfirm = await workspaceManager.getAll(); 2180 const confirmation = await getContextSwitchConfirmation( 2181 confirmPayload.targetParentId, 2182 allWsForConfirm 2183 ); 2184 sendResponse({ success: true, confirmation }); 2185 break; 2186 2187 // Sync management messages 2188 case 'INITIALIZE_SYNC': 2189 const initSyncPayload = message.payload as { backendType: SyncBackendType }; 2190 console.log('[Mnemonic] INITIALIZE_SYNC called with backend:', initSyncPayload.backendType); 2191 const syncManager = getSyncManager(); 2192 const initResult = await syncManager.initialize(initSyncPayload.backendType); 2193 console.log('[Mnemonic] Sync initialize result:', initResult); 2194 2195 // When switching to 'native', auto-pull from external if local is empty 2196 if (initResult && initSyncPayload.backendType === 'native') { 2197 const currentWorkspaces = await workspaceManager.getAll(); 2198 console.log('[Mnemonic] Current local workspaces count:', currentWorkspaces.length); 2199 if (currentWorkspaces.length === 0) { 2200 console.log('[Mnemonic] Native sync enabled with empty local storage, pulling from external...'); 2201 try { 2202 // Use forcePullFromExternal which bypasses change detection 2203 const pullResult = await syncManager.forcePullFromExternal(); 2204 console.log('[Mnemonic] Pulled existing sync data, result:', pullResult); 2205 } catch (error) { 2206 console.log('[Mnemonic] No existing sync data or pull failed:', error); 2207 } 2208 } else { 2209 console.log('[Mnemonic] Local storage has workspaces, skipping pull'); 2210 } 2211 } 2212 2213 sendResponse({ success: initResult, state: syncManager.getState() }); 2214 break; 2215 2216 case 'GET_SYNC_STATE': 2217 sendResponse({ state: getSyncManager().getState() }); 2218 break; 2219 2220 case 'SYNC_NOW': 2221 const syncNowManager = getSyncManager(); 2222 const currentConfig = await storage.read(); 2223 if (currentConfig) { 2224 const syncResult = await syncNowManager.forceSyncToExternal(currentConfig); 2225 sendResponse({ success: syncResult, state: syncNowManager.getState() }); 2226 } else { 2227 sendResponse({ success: false, error: 'No configuration to sync' }); 2228 } 2229 break; 2230 2231 case 'CHECK_EXTERNAL_CHANGES': 2232 try { 2233 const checkResult = await getSyncManager().checkAndMergeExternal(); 2234 sendResponse({ success: true, hasChanges: checkResult }); 2235 } catch (e) { 2236 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to check external changes' }); 2237 } 2238 break; 2239 2240 case 'TEST_SYNC_CONNECTION': 2241 try { 2242 const testSyncResult = await getSyncManager().testConnection(); 2243 sendResponse(testSyncResult); 2244 } catch (e) { 2245 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Connection test failed' }); 2246 } 2247 break; 2248 2249 case 'RESTORE_SYNC_BACKUP': 2250 try { 2251 const restoreResult = await getSyncManager().restoreFromBackup(); 2252 sendResponse({ success: restoreResult }); 2253 } catch (e) { 2254 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to restore backup' }); 2255 } 2256 break; 2257 2258 // Versioned backup management 2259 case 'GET_BACKUPS': 2260 try { 2261 const restBridgeForBackups = getRestBridge(); 2262 if (!await restBridgeForBackups.ensureConnected()) { 2263 sendResponse({ success: false, error: 'REST bridge not connected' }); 2264 break; 2265 } 2266 const backupListResult = await restBridgeForBackups.listBackups(); 2267 sendResponse(backupListResult); 2268 } catch (e) { 2269 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to list backups' }); 2270 } 2271 break; 2272 2273 case 'PREVIEW_BACKUP': 2274 try { 2275 const previewPayload = message.payload as { filename: string }; 2276 const restBridgeForPreview = getRestBridge(); 2277 if (!await restBridgeForPreview.ensureConnected()) { 2278 sendResponse({ success: false, error: 'REST bridge not connected' }); 2279 break; 2280 } 2281 const previewResult = await restBridgeForPreview.previewBackup(previewPayload.filename); 2282 sendResponse(previewResult); 2283 } catch (e) { 2284 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to preview backup' }); 2285 } 2286 break; 2287 2288 case 'RESTORE_VERSIONED_BACKUP': 2289 try { 2290 const restoreVersionedPayload = message.payload as { filename: string }; 2291 const restBridgeForRestore = getRestBridge(); 2292 if (!await restBridgeForRestore.ensureConnected()) { 2293 sendResponse({ success: false, error: 'REST bridge not connected' }); 2294 break; 2295 } 2296 const restoreVersionedResult = await restBridgeForRestore.restoreFromBackup(restoreVersionedPayload.filename); 2297 if (restoreVersionedResult.success) { 2298 // Reload from external to update local storage 2299 const syncMgr = getSyncManager(); 2300 await syncMgr.forcePullFromExternal(); 2301 } 2302 sendResponse(restoreVersionedResult); 2303 } catch (e) { 2304 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to restore from backup' }); 2305 } 2306 break; 2307 2308 case 'DELETE_BACKUP': 2309 try { 2310 const deleteBackupPayload = message.payload as { filename: string }; 2311 const restBridgeForDelete = getRestBridge(); 2312 if (!await restBridgeForDelete.ensureConnected()) { 2313 sendResponse({ success: false, error: 'REST bridge not connected' }); 2314 break; 2315 } 2316 const deleteBackupResult = await restBridgeForDelete.deleteBackup(deleteBackupPayload.filename); 2317 sendResponse(deleteBackupResult); 2318 } catch (e) { 2319 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to delete backup' }); 2320 } 2321 break; 2322 2323 case 'PRUNE_BACKUPS': 2324 try { 2325 const restBridgeForPrune = getRestBridge(); 2326 if (!await restBridgeForPrune.ensureConnected()) { 2327 sendResponse({ success: false, error: 'REST bridge not connected' }); 2328 break; 2329 } 2330 const pruneResult = await restBridgeForPrune.pruneBackups(); 2331 sendResponse(pruneResult); 2332 } catch (e) { 2333 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to prune backups' }); 2334 } 2335 break; 2336 2337 case 'SEARCH_BACKUPS': 2338 try { 2339 const searchBackupsPayload = message.payload as { query: string }; 2340 const restBridgeForSearch = getRestBridge(); 2341 if (!await restBridgeForSearch.ensureConnected()) { 2342 sendResponse({ success: false, error: 'REST bridge not connected' }); 2343 break; 2344 } 2345 const searchBackupsResult = await restBridgeForSearch.searchBackups(searchBackupsPayload.query); 2346 sendResponse(searchBackupsResult); 2347 } catch (e) { 2348 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to search backups' }); 2349 } 2350 break; 2351 2352 case 'RESTORE_SELECTIVE_BACKUP': 2353 try { 2354 const selectivePayload = message.payload as { filename: string; workspaceIds: string[] }; 2355 const restBridgeForSelective = getRestBridge(); 2356 if (!await restBridgeForSelective.ensureConnected()) { 2357 sendResponse({ success: false, error: 'REST bridge not connected' }); 2358 break; 2359 } 2360 const selectiveResult = await restBridgeForSelective.restoreSelectiveFromBackup( 2361 selectivePayload.filename, 2362 selectivePayload.workspaceIds 2363 ); 2364 if (selectiveResult.success) { 2365 // Reload from external to update local storage 2366 const syncMgr = getSyncManager(); 2367 await syncMgr.forcePullFromExternal(); 2368 } 2369 sendResponse(selectiveResult); 2370 } catch (e) { 2371 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to restore selective backup' }); 2372 } 2373 break; 2374 2375 case 'GET_AUDIBLE_TABS': 2376 try { 2377 // Get all tabs that are currently playing audio 2378 const allTabs = await browser.tabs.query({ audible: true }); 2379 2380 const windowTracker = getWindowTracker(); 2381 let mappingsArray = windowTracker.getMappings(); 2382 2383 // If mappings are empty (e.g., after extension reload), scan windows to rebuild them 2384 if (mappingsArray.length === 0) { 2385 const wsForScan = await workspaceManager.getAll(); 2386 await windowTracker.scanAllWindows(wsForScan); 2387 mappingsArray = windowTracker.getMappings(); 2388 } 2389 2390 // Map audible tabs to workspace IDs and collect detailed tab info 2391 const audibleWorkspaceIds: string[] = []; 2392 const audibleTabsDetailed: { tabId: number; windowId: number; title: string; url: string; workspaceId?: string }[] = []; 2393 2394 for (const tab of allTabs) { 2395 if (tab.windowId && tab.id) { 2396 // Find the workspace mapping for this window 2397 const mapping = mappingsArray.find(m => m.windowId === tab.windowId); 2398 const workspaceId = mapping?.workspaceId; 2399 if (workspaceId && !audibleWorkspaceIds.includes(workspaceId)) { 2400 audibleWorkspaceIds.push(workspaceId); 2401 } 2402 audibleTabsDetailed.push({ 2403 tabId: tab.id, 2404 windowId: tab.windowId, 2405 title: tab.title || 'Untitled', 2406 url: tab.url || '', 2407 workspaceId 2408 }); 2409 } 2410 } 2411 2412 // Also check if any parent workspaces have audible children 2413 const allWorkspaces = await workspaceManager.getAll(); 2414 const parentWithAudibleChildren: string[] = []; 2415 for (const ws of allWorkspaces) { 2416 if (ws.type === 'parent') { 2417 const children = allWorkspaces.filter( 2418 w => w.type === 'child' && (w as { parentId: string }).parentId === ws.id 2419 ); 2420 const hasAudibleChild = children.some(child => audibleWorkspaceIds.includes(child.id)); 2421 if (hasAudibleChild) { 2422 parentWithAudibleChildren.push(ws.id); 2423 } 2424 } 2425 } 2426 2427 sendResponse({ 2428 success: true, 2429 audibleWorkspaceIds, 2430 parentWithAudibleChildren, 2431 audibleTabs: audibleTabsDetailed 2432 }); 2433 } catch (e) { 2434 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to get audible tabs' }); 2435 } 2436 break; 2437 2438 case 'PAUSE_ALL_MEDIA': 2439 try { 2440 // Get all tabs 2441 const tabsToPause = await browser.tabs.query({}); 2442 let pausedCount = 0; 2443 2444 // Pause function to inject into tabs 2445 const pauseMediaFunction = () => { 2446 let count = 0; 2447 // Pause all video elements 2448 document.querySelectorAll('video').forEach((video) => { 2449 try { 2450 if (!(video as HTMLVideoElement).paused) { 2451 (video as HTMLVideoElement).pause(); 2452 count++; 2453 } 2454 } catch (e) { /* ignore */ } 2455 }); 2456 // Pause all audio elements 2457 document.querySelectorAll('audio').forEach((audio) => { 2458 try { 2459 if (!(audio as HTMLAudioElement).paused) { 2460 (audio as HTMLAudioElement).pause(); 2461 count++; 2462 } 2463 } catch (e) { /* ignore */ } 2464 }); 2465 // YouTube specific: simulate 'k' key 2466 if (window.location.hostname.includes('youtube.com')) { 2467 const player = document.querySelector('#movie_player') || document.querySelector('.html5-video-player'); 2468 if (player) { 2469 (player as HTMLElement).focus(); 2470 player.dispatchEvent(new KeyboardEvent('keydown', { 2471 key: 'k', code: 'KeyK', keyCode: 75, which: 75, bubbles: true, cancelable: true 2472 })); 2473 count++; 2474 } 2475 } 2476 return count; 2477 }; 2478 2479 // Try each tab - use scripting API to inject directly (works even without content script) 2480 for (const tab of tabsToPause) { 2481 if (tab.id && tab.url && (tab.url.startsWith('http://') || tab.url.startsWith('https://'))) { 2482 try { 2483 // First try sending message to content script 2484 await browser.tabs.sendMessage(tab.id, { type: 'PAUSE_MEDIA' }); 2485 pausedCount++; 2486 } catch { 2487 // Content script not loaded, inject and execute directly 2488 try { 2489 await browser.scripting.executeScript({ 2490 target: { tabId: tab.id }, 2491 func: pauseMediaFunction, 2492 }); 2493 pausedCount++; 2494 } catch { 2495 // Tab may be protected (chrome://, etc.), skip silently 2496 } 2497 } 2498 } 2499 } 2500 2501 sendResponse({ success: true, pausedCount }); 2502 } catch (e) { 2503 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to pause media' }); 2504 } 2505 break; 2506 2507 case 'FOCUS_TAB': 2508 try { 2509 const focusPayload = message.payload as { tabId: number; windowId: number }; 2510 // Focus the window first 2511 await browser.windows.update(focusPayload.windowId, { focused: true }); 2512 // Then activate the tab 2513 await browser.tabs.update(focusPayload.tabId, { active: true }); 2514 sendResponse({ success: true }); 2515 } catch (e) { 2516 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to focus tab' }); 2517 } 2518 break; 2519 2520 // Cross-browser workspace sharing 2521 case 'GET_SHARED_WORKSPACES': 2522 try { 2523 const restBridge = getRestBridge(); 2524 const sharedSyncManager = getSyncManager(); 2525 2526 // Ensure connected 2527 if (!restBridge.isConnected()) { 2528 const connected = await restBridge.connect(); 2529 if (!connected) { 2530 sendResponse({ success: false, error: 'REST bridge not connected' }); 2531 break; 2532 } 2533 } 2534 2535 // Push current browser's workspaces first to ensure they're up to date 2536 // This ensures the Cross-Browser modal shows the latest workspaces 2537 if (sharedSyncManager.getBackendType() === 'native') { 2538 const currentConfig = await storage.read(); 2539 if (currentConfig) { 2540 console.log('[Mnemonic] Pushing current workspaces before reading shared file...'); 2541 await sharedSyncManager.forceSyncToExternal(currentConfig); 2542 } 2543 } 2544 2545 // Read shared file 2546 const readResult = await restBridge.read(); 2547 if (!readResult.success) { 2548 sendResponse({ success: false, error: 'Failed to read shared file' }); 2549 break; 2550 } 2551 2552 // Parse or create default shared file structure 2553 let sharedData: SharedWorkspaceFile; 2554 if (readResult.data && typeof readResult.data === 'object') { 2555 sharedData = readResult.data as SharedWorkspaceFile; 2556 // Ensure browsers object exists 2557 if (!sharedData.browsers) { 2558 sharedData.browsers = {}; 2559 } 2560 } else { 2561 sharedData = { ...DEFAULT_SHARED_FILE, lastModified: new Date().toISOString(), browsers: {} }; 2562 } 2563 2564 sendResponse({ success: true, data: sharedData }); 2565 } catch (e) { 2566 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to get shared workspaces' }); 2567 } 2568 break; 2569 2570 case 'PULL_WORKSPACES': 2571 try { 2572 const pullPayload = message.payload as { sourceBrowser: BrowserKey; workspaceIds: string[] }; 2573 const restBridge = getRestBridge(); 2574 2575 // Ensure connected 2576 if (!restBridge.isConnected()) { 2577 const connected = await restBridge.connect(); 2578 if (!connected) { 2579 sendResponse({ success: false, error: 'REST bridge not connected' }); 2580 break; 2581 } 2582 } 2583 2584 // Read shared file 2585 const readResult = await restBridge.read(); 2586 if (!readResult.success || !readResult.data) { 2587 sendResponse({ success: false, error: 'No shared data available' }); 2588 break; 2589 } 2590 2591 const sharedData = readResult.data as SharedWorkspaceFile; 2592 const sourceBrowserData = sharedData.browsers[pullPayload.sourceBrowser]; 2593 if (!sourceBrowserData) { 2594 sendResponse({ success: false, error: `No workspaces from ${pullPayload.sourceBrowser}` }); 2595 break; 2596 } 2597 2598 // Find workspaces to pull 2599 const workspacesToPull = sourceBrowserData.workspaces.filter( 2600 ws => pullPayload.workspaceIds.includes(ws.id) 2601 ); 2602 2603 // Also include children if pulling a parent 2604 const childIds: string[] = []; 2605 for (const ws of workspacesToPull) { 2606 if (ws.type === 'parent' && ws.children) { 2607 childIds.push(...ws.children); 2608 } 2609 } 2610 const childWorkspaces = sourceBrowserData.workspaces.filter(ws => childIds.includes(ws.id)); 2611 const allToPull = [...workspacesToPull, ...childWorkspaces]; 2612 2613 // Generate new IDs and create ID mapping 2614 const idMapping = new Map<string, string>(); 2615 for (const ws of allToPull) { 2616 idMapping.set(ws.id, crypto.randomUUID()); 2617 } 2618 2619 // Create new workspaces with remapped IDs 2620 const config = await storage.read(); 2621 if (!config) { 2622 sendResponse({ success: false, error: 'Failed to read storage' }); 2623 break; 2624 } 2625 2626 for (const ws of allToPull) { 2627 const newId = idMapping.get(ws.id)!; 2628 const newParentId = ws.parentId ? (idMapping.get(ws.parentId) || ws.parentId) : null; 2629 const newChildren = ws.children?.map(childId => idMapping.get(childId) || childId); 2630 2631 const newWorkspace = { 2632 ...ws, 2633 id: newId, 2634 parentId: newParentId, 2635 children: newChildren, 2636 metadata: { 2637 ...ws.metadata, 2638 created: new Date().toISOString(), 2639 lastModified: new Date().toISOString(), 2640 lastAccessed: new Date().toISOString(), 2641 }, 2642 }; 2643 2644 config.workspaces.push(newWorkspace as any); 2645 } 2646 2647 // Save 2648 await storage.write(config); 2649 2650 // Rebuild search index 2651 searchIndex = buildIndex(config.workspaces); 2652 2653 sendResponse({ success: true, pulledCount: allToPull.length }); 2654 } catch (e) { 2655 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to pull workspaces' }); 2656 } 2657 break; 2658 2659 case 'CHECK_BRIDGE_STATUS': 2660 try { 2661 const restBridge = getRestBridge(); 2662 const isConnected = restBridge.isConnected(); 2663 console.log('[Mnemonic] Bridge status check - isConnected:', isConnected); 2664 2665 if (!isConnected) { 2666 // Try to connect 2667 console.log('[Mnemonic] Attempting to connect to REST bridge...'); 2668 const connected = await restBridge.connect(); 2669 console.log('[Mnemonic] Connection result:', connected); 2670 sendResponse({ success: true, connected, browserKey: detectBrowserKey() }); 2671 } else { 2672 sendResponse({ success: true, connected: true, browserKey: detectBrowserKey() }); 2673 } 2674 } catch (e) { 2675 console.error('[Mnemonic] Bridge status check failed:', e); 2676 sendResponse({ success: true, connected: false }); 2677 } 2678 break; 2679 2680 case 'SEND_TAB_TRANSFER': 2681 try { 2682 const transferPayload = message.payload as { 2683 target_browser: string; 2684 target_workspace_id: string; 2685 target_workspace_name: string; 2686 tab: { url: string; title: string; pinned?: boolean }; 2687 }; 2688 const transferBridge = getRestBridge(); 2689 if (!transferBridge.isConnected()) { 2690 const ok = await transferBridge.connect(); 2691 if (!ok) { 2692 sendResponse({ success: false, error: 'Bridge not connected' }); 2693 break; 2694 } 2695 } 2696 const transferResult = await transferBridge.sendTransfer({ 2697 source_browser: detectBrowserKey(), 2698 target_browser: transferPayload.target_browser, 2699 target_workspace_id: transferPayload.target_workspace_id, 2700 target_workspace_name: transferPayload.target_workspace_name, 2701 tab: transferPayload.tab, 2702 }); 2703 sendResponse({ success: transferResult.success, transfer_id: transferResult.transfer_id }); 2704 } catch (e) { 2705 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Transfer failed' }); 2706 } 2707 break; 2708 2709 // Archive/Restore management 2710 case 'ARCHIVE_WORKSPACE': 2711 try { 2712 const archivePayload = message.payload as { id: string }; 2713 await workspaceManager.archive(archivePayload.id); 2714 removeWorkspaceFromIndex(searchIndex, archivePayload.id); 2715 debouncedSaveSearchIndex(); 2716 sendResponse({ success: true }); 2717 broadcastWorkspaceStatusChange(); 2718 // Rebuild context menus to remove archived workspace 2719 buildContextMenus(); 2720 } catch (e) { 2721 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to archive workspace' }); 2722 } 2723 break; 2724 2725 case 'ARCHIVE_CONTEXT': 2726 try { 2727 const archiveContextPayload = message.payload as { parentId: string }; 2728 const archiveResult = await workspaceManager.archiveContext(archiveContextPayload.parentId); 2729 // Remove archived workspaces from search index 2730 const archivedWs = await workspaceManager.getArchived(); 2731 archivedWs.forEach((ws) => removeWorkspaceFromIndex(searchIndex, ws.id)); 2732 debouncedSaveSearchIndex(); 2733 sendResponse({ success: true, archivedCount: archiveResult.archivedCount }); 2734 broadcastWorkspaceStatusChange(); 2735 // Rebuild context menus to remove archived workspaces 2736 buildContextMenus(); 2737 } catch (e) { 2738 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to archive context' }); 2739 } 2740 break; 2741 2742 case 'RESTORE_WORKSPACE': 2743 try { 2744 const restorePayload = message.payload as { id: string }; 2745 await workspaceManager.restore(restorePayload.id); 2746 const restoredWs = await workspaceManager.getById(restorePayload.id); 2747 if (restoredWs) { 2748 updateWorkspaceInIndex(searchIndex, restoredWs); 2749 } 2750 debouncedSaveSearchIndex(); 2751 sendResponse({ success: true }); 2752 broadcastWorkspaceStatusChange(); 2753 // Rebuild context menus to include restored workspace 2754 buildContextMenus(); 2755 } catch (e) { 2756 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to restore workspace' }); 2757 } 2758 break; 2759 2760 case 'RESTORE_CONTEXT': 2761 try { 2762 const restoreContextPayload = message.payload as { parentId: string }; 2763 const restoreResult = await workspaceManager.restoreContext(restoreContextPayload.parentId); 2764 // Re-index restored workspaces 2765 const activeWs = await workspaceManager.getActive(); 2766 searchIndex = buildIndex(activeWs); 2767 debouncedSaveSearchIndex(); 2768 sendResponse({ success: true, restoredCount: restoreResult.restoredCount }); 2769 broadcastWorkspaceStatusChange(); 2770 // Rebuild context menus to include restored workspaces 2771 buildContextMenus(); 2772 } catch (e) { 2773 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to restore context' }); 2774 } 2775 break; 2776 2777 case 'PERMANENTLY_DELETE_WORKSPACE': 2778 try { 2779 const permDeletePayload = message.payload as { id: string }; 2780 await workspaceManager.permanentlyDelete(permDeletePayload.id); 2781 sendResponse({ success: true }); 2782 broadcastWorkspaceStatusChange(); 2783 // Rebuild context menus (in case workspace was still in menus) 2784 buildContextMenus(); 2785 } catch (e) { 2786 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to permanently delete workspace' }); 2787 } 2788 break; 2789 2790 case 'GET_ARCHIVED_WORKSPACES': 2791 try { 2792 const archivedWorkspaces = await workspaceManager.getArchived(); 2793 sendResponse(archivedWorkspaces); 2794 } catch (e) { 2795 sendResponse({ error: e instanceof Error ? e.message : 'Failed to get archived workspaces' }); 2796 } 2797 break; 2798 2799 // Bookmark export/import 2800 case 'EXPORT_TO_BOOKMARKS': 2801 try { 2802 const exportPayload = message.payload as { workspaceId: string }; 2803 const wsToExport = await workspaceManager.getById(exportPayload.workspaceId); 2804 if (!wsToExport) { 2805 sendResponse({ success: false, error: 'Workspace not found' }); 2806 break; 2807 } 2808 if (wsToExport.type === 'parent') { 2809 sendResponse({ success: false, error: 'Use EXPORT_CONTEXT_TO_BOOKMARKS for parent workspaces' }); 2810 break; 2811 } 2812 const exportResult = await exportWorkspaceToBookmarks(wsToExport as ChildWorkspace | StandaloneWorkspace); 2813 sendResponse(exportResult); 2814 } catch (e) { 2815 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to export to bookmarks' }); 2816 } 2817 break; 2818 2819 case 'EXPORT_CONTEXT_TO_BOOKMARKS': 2820 try { 2821 const exportContextPayload = message.payload as { parentId: string }; 2822 const parentWs = await workspaceManager.getById(exportContextPayload.parentId); 2823 if (!parentWs || parentWs.type !== 'parent') { 2824 sendResponse({ success: false, error: 'Parent workspace not found' }); 2825 break; 2826 } 2827 const allWorkspaces = await workspaceManager.getAll(); 2828 const childWorkspaces = allWorkspaces.filter( 2829 (ws): ws is ChildWorkspace => ws.type === 'child' && ws.parentId === exportContextPayload.parentId 2830 ); 2831 const contextExportResult = await exportContextToBookmarks(parentWs as ParentWorkspace, childWorkspaces); 2832 sendResponse(contextExportResult); 2833 } catch (e) { 2834 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to export context to bookmarks' }); 2835 } 2836 break; 2837 2838 case 'GET_BOOKMARK_FOLDERS': 2839 try { 2840 const folders = await getMnemonicBookmarkFolders(); 2841 const foldersWithStructure = folders.map(folder => ({ 2842 ...folder, 2843 structure: detectStructure(folder), 2844 recommendedMode: getRecommendedImportMode(folder) 2845 })); 2846 sendResponse({ success: true, folders: foldersWithStructure }); 2847 } catch (e) { 2848 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to get bookmark folders' }); 2849 } 2850 break; 2851 2852 case 'IMPORT_FROM_BOOKMARKS': 2853 try { 2854 const importPayload = message.payload as { 2855 folderId: string; 2856 folderData: import('../lib/bookmarks').BookmarkFolder; 2857 mode: import('../lib/bookmarks').ImportMode; 2858 parentId?: string; // Optional: import as child of existing parent 2859 }; 2860 2861 const { folderData, mode, parentId } = importPayload; 2862 const effectiveMode = mode === 'auto' ? getRecommendedImportMode(folderData) : mode; 2863 2864 if (effectiveMode === 'standalone' || parentId) { 2865 // Import as standalone or as child of existing parent 2866 const tabs = importAsStandalone(folderData); 2867 if (tabs.length === 0) { 2868 sendResponse({ success: false, error: 'No valid bookmarks to import' }); 2869 break; 2870 } 2871 2872 const newWorkspace = await workspaceManager.create({ 2873 name: folderData.title, 2874 type: parentId ? 'child' : 'standalone', 2875 parentId: parentId || null, 2876 tabs 2877 }); 2878 2879 updateWorkspaceInIndex(searchIndex, newWorkspace); 2880 debouncedSaveSearchIndex(); 2881 sendResponse({ 2882 success: true, 2883 workspaceId: newWorkspace.id, 2884 tabCount: tabs.length 2885 }); 2886 broadcastWorkspaceStatusChange(); 2887 // Rebuild context menus to include imported workspace 2888 buildContextMenus(); 2889 } else { 2890 // Import as context (parent + children) 2891 const contextData = importAsContext(folderData); 2892 if (contextData.children.length === 0 || contextData.children.every(c => c.tabs.length === 0)) { 2893 sendResponse({ success: false, error: 'No valid bookmarks to import' }); 2894 break; 2895 } 2896 2897 // Create parent workspace 2898 const parentWorkspace = await workspaceManager.create({ 2899 name: contextData.parentName, 2900 type: 'parent', 2901 parentId: null 2902 }); 2903 2904 // Create child workspaces 2905 let totalTabs = 0; 2906 for (const childData of contextData.children) { 2907 if (childData.tabs.length > 0) { 2908 const childWorkspace = await workspaceManager.create({ 2909 name: childData.name, 2910 type: 'child', 2911 parentId: parentWorkspace.id, 2912 tabs: childData.tabs 2913 }); 2914 updateWorkspaceInIndex(searchIndex, childWorkspace); 2915 totalTabs += childData.tabs.length; 2916 } 2917 } 2918 2919 updateWorkspaceInIndex(searchIndex, parentWorkspace); 2920 debouncedSaveSearchIndex(); 2921 sendResponse({ 2922 success: true, 2923 parentId: parentWorkspace.id, 2924 childrenCount: contextData.children.filter(c => c.tabs.length > 0).length, 2925 tabCount: totalTabs 2926 }); 2927 broadcastWorkspaceStatusChange(); 2928 // Rebuild context menus to include imported workspaces 2929 buildContextMenus(); 2930 } 2931 } catch (e) { 2932 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to import from bookmarks' }); 2933 } 2934 break; 2935 2936 case 'MOVE_TAB': 2937 try { 2938 const moveTabPayload = message.payload as { 2939 sourceWorkspaceId: string; 2940 targetWorkspaceId: string; 2941 tab: { url: string; title: string; pinned?: boolean }; 2942 tabIndex: number; 2943 }; 2944 2945 // Get both workspaces 2946 const sourceWs = await workspaceManager.getById(moveTabPayload.sourceWorkspaceId); 2947 const targetWs = await workspaceManager.getById(moveTabPayload.targetWorkspaceId); 2948 2949 if (!sourceWs) { 2950 sendResponse({ success: false, error: 'Source workspace not found' }); 2951 break; 2952 } 2953 if (!targetWs) { 2954 sendResponse({ success: false, error: 'Target workspace not found' }); 2955 break; 2956 } 2957 2958 // Validate both are tab workspaces 2959 if (!isTabWorkspace(sourceWs)) { 2960 sendResponse({ success: false, error: 'Source workspace cannot contain tabs' }); 2961 break; 2962 } 2963 if (!isTabWorkspace(targetWs)) { 2964 sendResponse({ success: false, error: 'Target workspace cannot contain tabs' }); 2965 break; 2966 } 2967 2968 const sourceTabWs = sourceWs as ChildWorkspace | StandaloneWorkspace; 2969 const targetTabWs = targetWs as ChildWorkspace | StandaloneWorkspace; 2970 2971 // Remove the tab from source workspace 2972 const sourceTabs = [...sourceTabWs.tabs]; 2973 if (moveTabPayload.tabIndex < 0 || moveTabPayload.tabIndex >= sourceTabs.length) { 2974 sendResponse({ success: false, error: 'Invalid tab index' }); 2975 break; 2976 } 2977 sourceTabs.splice(moveTabPayload.tabIndex, 1); 2978 await workspaceManager.updateTabs(moveTabPayload.sourceWorkspaceId, sourceTabs, { allowEmpty: true }); 2979 2980 // Add the tab to target workspace 2981 const targetTabs = [...targetTabWs.tabs, moveTabPayload.tab]; 2982 await workspaceManager.updateTabs(moveTabPayload.targetWorkspaceId, targetTabs); 2983 2984 // Check if source workspace window is open and close the tab 2985 const windowTracker = getWindowTracker(); 2986 const sourceWindowId = windowTracker.getWorkspaceWindow(moveTabPayload.sourceWorkspaceId); 2987 if (sourceWindowId !== undefined) { 2988 try { 2989 // Find the tab in the source window by URL 2990 const windowTabs = await browser.tabs.query({ windowId: sourceWindowId }); 2991 const tabToClose = windowTabs.find(t => t.url === moveTabPayload.tab.url); 2992 if (tabToClose?.id) { 2993 await browser.tabs.remove(tabToClose.id); 2994 } 2995 } catch (e) { 2996 console.warn('[Mnemonic] Failed to close tab in source window:', e); 2997 } 2998 } 2999 3000 // Check if target workspace window is open and open the tab in background 3001 const targetWindowId = windowTracker.getWorkspaceWindow(moveTabPayload.targetWorkspaceId); 3002 if (targetWindowId !== undefined) { 3003 try { 3004 await browser.tabs.create({ 3005 windowId: targetWindowId, 3006 url: moveTabPayload.tab.url, 3007 pinned: moveTabPayload.tab.pinned || false, 3008 active: false // Open in background 3009 }); 3010 } catch (e) { 3011 console.warn('[Mnemonic] Failed to open tab in target window:', e); 3012 } 3013 } 3014 3015 // Update search indices 3016 const updatedSource = await workspaceManager.getById(moveTabPayload.sourceWorkspaceId); 3017 const updatedTarget = await workspaceManager.getById(moveTabPayload.targetWorkspaceId); 3018 if (updatedSource) updateWorkspaceInIndex(searchIndex, updatedSource); 3019 if (updatedTarget) updateWorkspaceInIndex(searchIndex, updatedTarget); 3020 debouncedSaveSearchIndex(); 3021 3022 sendResponse({ success: true }); 3023 broadcastWorkspaceStatusChange(); 3024 } catch (e) { 3025 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to move tab' }); 3026 } 3027 break; 3028 3029 case 'RESCAN_WINDOW_MAPPINGS': 3030 // Manually trigger window-to-workspace matching (normally only done at startup) 3031 try { 3032 console.log('[Mnemonic] Manual rescan of window mappings requested'); 3033 await scanWindowsAndCreateDashboardTabs(); 3034 sendResponse({ success: true }); 3035 broadcastWorkspaceStatusChange(); 3036 } catch (e) { 3037 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to rescan window mappings' }); 3038 } 3039 break; 3040 3041 case 'CLEAR_MANUAL_MAPPINGS': 3042 // Clear manual workspace-window assignments (unlock workspaces for auto-matching) 3043 try { 3044 const clearPayload = message.payload as { workspaceId?: string } | undefined; 3045 const allWorkspaces = await workspaceManager.getAll(); 3046 3047 if (clearPayload?.workspaceId) { 3048 // Clear single workspace 3049 const ws = allWorkspaces.find(w => w.id === clearPayload.workspaceId); 3050 if (ws && (ws.type === 'child' || ws.type === 'standalone')) { 3051 await workspaceManager.update(ws.id, { manuallyAssignedWindowId: null }); 3052 console.log('[Mnemonic] Cleared manual mapping for workspace:', ws.name); 3053 } 3054 } else { 3055 // Clear all workspaces 3056 for (const ws of allWorkspaces) { 3057 if ((ws.type === 'child' || ws.type === 'standalone') && 3058 (ws as { manuallyAssignedWindowId?: number }).manuallyAssignedWindowId) { 3059 await workspaceManager.update(ws.id, { manuallyAssignedWindowId: null }); 3060 console.log('[Mnemonic] Cleared manual mapping for workspace:', ws.name); 3061 } 3062 } 3063 } 3064 sendResponse({ success: true }); 3065 broadcastWorkspaceStatusChange(); 3066 } catch (e) { 3067 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to clear manual mappings' }); 3068 } 3069 break; 3070 3071 case 'SET_MANUAL_MAPPING': 3072 // Mark a workspace as manually assigned to current window (locked mapping) 3073 try { 3074 const setMappingPayload = message.payload as { workspaceId: string; windowId: number }; 3075 await workspaceManager.update(setMappingPayload.workspaceId, { 3076 manuallyAssignedWindowId: setMappingPayload.windowId, 3077 }); 3078 console.log('[Mnemonic] Set manual mapping for workspace:', setMappingPayload.workspaceId, '→ window:', setMappingPayload.windowId); 3079 sendResponse({ success: true }); 3080 broadcastWorkspaceStatusChange(); 3081 } catch (e) { 3082 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to set manual mapping' }); 3083 } 3084 break; 3085 3086 // ===================================================================== 3087 // Credential Vault Management 3088 // ===================================================================== 3089 3090 case 'VAULT_STATUS': 3091 try { 3092 const { getCredentialVault } = await import('../lib/crypto'); 3093 const { getCredentialsSync } = await import('../lib/sync/credentials-sync'); 3094 3095 const vault = getCredentialVault(); 3096 vault.setCredentialsSync(getCredentialsSync()); 3097 const vaultStatus = await vault.getStatus(); 3098 sendResponse({ success: true, ...vaultStatus }); 3099 } catch (e) { 3100 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to get vault status' }); 3101 } 3102 break; 3103 3104 case 'VAULT_SETUP': 3105 try { 3106 const setupPayload = message.payload as { password: string }; 3107 const { getCredentialVault } = await import('../lib/crypto'); 3108 const { getCredentialsSync } = await import('../lib/sync/credentials-sync'); 3109 3110 const vault = getCredentialVault(); 3111 vault.setCredentialsSync(getCredentialsSync()); 3112 await vault.setupMasterPassword(setupPayload.password); 3113 3114 sendResponse({ success: true }); 3115 } catch (e) { 3116 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to setup master password' }); 3117 } 3118 break; 3119 3120 case 'VAULT_UNLOCK': 3121 try { 3122 const unlockPayload = message.payload as { password: string }; 3123 const { getCredentialVault } = await import('../lib/crypto'); 3124 const { getCredentialsSync } = await import('../lib/sync/credentials-sync'); 3125 3126 const vault = getCredentialVault(); 3127 vault.setCredentialsSync(getCredentialsSync()); 3128 const unlockSuccess = await vault.unlock(unlockPayload.password); 3129 3130 sendResponse({ success: unlockSuccess, error: unlockSuccess ? undefined : 'Incorrect password' }); 3131 } catch (e) { 3132 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to unlock vault' }); 3133 } 3134 break; 3135 3136 case 'VAULT_LOCK': 3137 try { 3138 const { getCredentialVault } = await import('../lib/crypto'); 3139 const vault = getCredentialVault(); 3140 await vault.lock(); 3141 sendResponse({ success: true }); 3142 } catch (e) { 3143 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to lock vault' }); 3144 } 3145 break; 3146 3147 case 'VAULT_STORE_CREDENTIAL': 3148 try { 3149 const storeCredPayload = message.payload as { 3150 type: 'claude' | 'todoist' | 'logseq'; 3151 credentials: unknown; 3152 }; 3153 const { getCredentialVault } = await import('../lib/crypto'); 3154 const { getCredentialsSync } = await import('../lib/sync/credentials-sync'); 3155 3156 const vault = getCredentialVault(); 3157 vault.setCredentialsSync(getCredentialsSync()); 3158 3159 await vault.storeCredential(storeCredPayload.type, storeCredPayload.credentials as any); 3160 sendResponse({ success: true }); 3161 } catch (e) { 3162 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to store credential' }); 3163 } 3164 break; 3165 3166 case 'VAULT_GET_CREDENTIAL': 3167 try { 3168 const getCredPayload = message.payload as { type: 'claude' | 'todoist' | 'logseq' }; 3169 const { getCredentialVault } = await import('../lib/crypto'); 3170 const { getCredentialsSync } = await import('../lib/sync/credentials-sync'); 3171 3172 const vault = getCredentialVault(); 3173 vault.setCredentialsSync(getCredentialsSync()); 3174 3175 const credential = await vault.getCredential(getCredPayload.type); 3176 sendResponse({ success: true, credential }); 3177 } catch (e) { 3178 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to get credential' }); 3179 } 3180 break; 3181 3182 case 'VAULT_REMOVE_CREDENTIAL': 3183 try { 3184 const removeCredPayload = message.payload as { type: 'claude' | 'todoist' | 'logseq' }; 3185 const { getCredentialVault } = await import('../lib/crypto'); 3186 const { getCredentialsSync } = await import('../lib/sync/credentials-sync'); 3187 3188 const vault = getCredentialVault(); 3189 vault.setCredentialsSync(getCredentialsSync()); 3190 3191 await vault.removeCredential(removeCredPayload.type); 3192 sendResponse({ success: true }); 3193 } catch (e) { 3194 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to remove credential' }); 3195 } 3196 break; 3197 3198 case 'VAULT_CHANGE_PASSWORD': 3199 try { 3200 const changePassPayload = message.payload as { oldPassword: string; newPassword: string }; 3201 const { getCredentialVault } = await import('../lib/crypto'); 3202 const { getCredentialsSync } = await import('../lib/sync/credentials-sync'); 3203 3204 const vault = getCredentialVault(); 3205 vault.setCredentialsSync(getCredentialsSync()); 3206 3207 await vault.changeMasterPassword(changePassPayload.oldPassword, changePassPayload.newPassword); 3208 sendResponse({ success: true }); 3209 } catch (e) { 3210 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to change password' }); 3211 } 3212 break; 3213 3214 // ===================================================================== 3215 // Debug Logging 3216 // ===================================================================== 3217 3218 case 'GET_DEBUG_LOGS': 3219 try { 3220 const debugLogger = getDebugLogger(); 3221 const entries = debugLogger.getEntries(); 3222 sendResponse({ success: true, entries }); 3223 } catch (e) { 3224 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to get debug logs' }); 3225 } 3226 break; 3227 3228 case 'CLEAR_DEBUG_LOGS': 3229 try { 3230 await getDebugLogger().clear(); 3231 sendResponse({ success: true }); 3232 } catch (e) { 3233 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to clear debug logs' }); 3234 } 3235 break; 3236 3237 case 'EXPORT_DEBUG_LOGS': 3238 try { 3239 const exported = getDebugLogger().export(); 3240 sendResponse({ success: true, data: exported }); 3241 } catch (e) { 3242 sendResponse({ success: false, error: e instanceof Error ? e.message : 'Failed to export debug logs' }); 3243 } 3244 break; 3245 3246 default: 3247 sendResponse({ error: `Unknown message type: ${message.type}` }); 3248 } 3249 } catch (error) { 3250 const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 3251 console.error('[Mnemonic] Message handler error:', errorMessage); 3252 sendResponse({ error: errorMessage }); 3253 } 3254 }