/ src / entrypoints / background.ts
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  }