/ src / entrypoints / options / App.svelte
App.svelte
   1  <script lang="ts">
   2    import { onMount } from 'svelte';
   3    import { isFileSystemAccessSupported, getFilesystemBridge } from '../../lib/sync/filesystem-bridge';
   4    import { getLogseqClient, type LogseqSettings, DEFAULT_LOGSEQ_SETTINGS } from '../../lib/logseq';
   5    import { getFileSystemService, isFileSystemAccessSupported as isGraphAccessSupported } from '../../lib/filesystem';
   6    import { DEFAULT_PAUSE_MEDIA_SITES } from '../../lib/pause-media-defaults';
   7    import { todoistAuth, todoistClient, workspaceSyncService } from '../../lib/todoist';
   8    import type { TodoistSettings, TodoistConnectionState } from '../../lib/todoist';
   9  
  10    interface Settings {
  11      autoSave: boolean;
  12      autoSaveInterval: number;
  13      showOrphanPrompt: boolean;
  14      syncBackend: 'none' | 'native' | 'filesystem';
  15      confirmContextSwitch: boolean;
  16      restoreWindowPositions: boolean;
  17      useSmartPositioning: boolean;
  18      theme: 'system' | 'light' | 'dark';
  19      lazyLoadTabs: boolean;
  20      pauseMediaSites?: string[];
  21    }
  22  
  23    let settings = $state<Settings>({
  24      autoSave: true,
  25      autoSaveInterval: 30000,
  26      showOrphanPrompt: true,
  27      syncBackend: 'none',
  28      confirmContextSwitch: true,
  29      restoreWindowPositions: true,
  30      useSmartPositioning: true,
  31      theme: 'system',
  32      lazyLoadTabs: true,
  33      pauseMediaSites: [],
  34    });
  35  
  36    // Pause media sites editing
  37    let pauseMediaEditMode = $state(false);
  38    let pauseMediaText = $state('');
  39    let pauseMediaSaved = $state(false);
  40  
  41    let saving = $state(false);
  42    let saved = $state(false);
  43  
  44    // Filesystem sync
  45    const filesystemSupported = isFileSystemAccessSupported();
  46    const filesystemBridge = getFilesystemBridge();
  47    let filesystemConnected = $state(false);
  48    let filesystemDirectory = $state<string | null>(null);
  49    let selectingDirectory = $state(false);
  50    let filesystemError = $state<string | null>(null);
  51  
  52    // Native messaging sync
  53    let nativeConnected = $state(false);
  54    let nativeConfigDir = $state<string | null>(null);
  55    let nativeLastSync = $state<string | null>(null);
  56    let nativeTesting = $state(false);
  57    let nativeSyncing = $state(false);
  58    let nativeError = $state<string | null>(null);
  59  
  60    // Claude settings
  61    let claudeApiKey = $state('');
  62    let claudeEnabled = $state(false);
  63    let claudeAutoSuggest = $state(false);
  64    let claudeTesting = $state(false);
  65    let claudeTestResult = $state<'success' | 'error' | null>(null);
  66    let claudeTestError = $state('');
  67  
  68    // Logseq settings
  69    const logseqClient = getLogseqClient();
  70    let logseqSettings = $state<LogseqSettings>({ ...DEFAULT_LOGSEQ_SETTINGS });
  71    let logseqConnected = $state(false);
  72    let logseqGraphName = $state<string | null>(null);
  73    let logseqTesting = $state(false);
  74    let logseqTestResult = $state<'success' | 'error' | null>(null);
  75  
  76    // Logseq graph access (for file attachments)
  77    const graphAccessSupported = isGraphAccessSupported();
  78    const graphAccessService = getFileSystemService();
  79    let graphAccessGranted = $state(false);
  80    let graphAccessDirectory = $state<string | null>(null);
  81    let graphAccessRequesting = $state(false);
  82  
  83    // Todoist settings
  84    let todoistEnabled = $state(false);
  85    let todoistConnection = $state<TodoistConnectionState | null>(null);
  86    let todoistApiToken = $state('');
  87    let todoistTesting = $state(false);
  88    let todoistTestResult = $state<'success' | 'error' | null>(null);
  89    let todoistAutoSync = $state(true);
  90  
  91    // Credential vault settings
  92    let vaultExists = $state(false);
  93    let vaultUnlocked = $state(false);
  94    let vaultCredentials = $state({ claude: false, todoist: false, logseq: false });
  95    let vaultSetupMode = $state(false);
  96    let vaultChangingPassword = $state(false);
  97    let vaultOldPassword = $state('');
  98    let vaultNewPassword = $state('');
  99    let vaultConfirmPassword = $state('');
 100    let vaultError = $state<string | null>(null);
 101    let vaultSuccess = $state<string | null>(null);
 102  
 103    onMount(async () => {
 104      const result = await browser.storage.local.get('workspaceConfig');
 105      if (result.workspaceConfig?.settings) {
 106        settings = { ...settings, ...result.workspaceConfig.settings };
 107      }
 108  
 109      // Check filesystem sync status
 110      if (settings.syncBackend === 'filesystem' && filesystemSupported) {
 111        const connected = await filesystemBridge.connect();
 112        filesystemConnected = connected;
 113        if (connected) {
 114          const state = filesystemBridge.getState();
 115          filesystemDirectory = state.configDir || null;
 116        }
 117      }
 118  
 119      // Check native messaging sync status
 120      if (settings.syncBackend === 'native') {
 121        await checkNativeConnection();
 122      }
 123  
 124      // Load Claude settings
 125      try {
 126        const claudeResponse = await browser.runtime.sendMessage({ type: 'GET_CLAUDE_SETTINGS' });
 127        claudeEnabled = claudeResponse.enabled || false;
 128        claudeAutoSuggest = claudeResponse.autoSuggest || false;
 129        // API key is masked, so we don't set it
 130      } catch {
 131        // Claude not configured
 132      }
 133  
 134      // Load Logseq settings
 135      try {
 136        const stored = await browser.storage.local.get('logseqSettings');
 137        if (stored.logseqSettings) {
 138          logseqSettings = { ...logseqSettings, ...stored.logseqSettings };
 139          // Create plain object copy to avoid Firefox proxy serialization issues
 140          logseqClient.setSettings({ ...logseqSettings });
 141        }
 142  
 143        // Check connection if enabled
 144        if (logseqSettings.enabled) {
 145          const state = await logseqClient.checkConnection();
 146          logseqConnected = state.available;
 147          logseqGraphName = state.graphName;
 148        }
 149  
 150        // Restore graph access for attachments
 151        if (graphAccessSupported) {
 152          const hasAccess = await graphAccessService.restoreAccess();
 153          const graphState = graphAccessService.getState();
 154          graphAccessGranted = hasAccess;
 155          graphAccessDirectory = graphState.directoryPath;
 156        }
 157      } catch {
 158        // Logseq not configured
 159      }
 160  
 161      // Load Todoist settings
 162      try {
 163        const todoistSettings = await todoistAuth.loadSettings();
 164        todoistEnabled = todoistSettings.enabled || false;
 165  
 166        if (todoistEnabled && todoistSettings.accessToken) {
 167          todoistConnection = await todoistClient.initializeFromStorage();
 168        }
 169      } catch {
 170        // Todoist not configured
 171      }
 172  
 173      // Load vault status and credentials if unlocked
 174      await loadVaultStatus();
 175      if (vaultUnlocked && (vaultCredentials.claude || vaultCredentials.todoist || vaultCredentials.logseq)) {
 176        await loadCredentialsFromVault();
 177      }
 178    });
 179  
 180    async function loadVaultStatus() {
 181      try {
 182        const vaultResponse = await browser.runtime.sendMessage({ type: 'VAULT_STATUS' });
 183        if (vaultResponse.success) {
 184          vaultExists = vaultResponse.exists;
 185          vaultUnlocked = vaultResponse.unlocked;
 186          vaultCredentials = vaultResponse.credentials || { claude: false, todoist: false, logseq: false };
 187        }
 188      } catch {
 189        // Vault not available (native bridge not connected)
 190      }
 191    }
 192  
 193    async function loadCredentialsFromVault() {
 194      // Load Claude credentials from vault
 195      if (vaultCredentials.claude) {
 196        try {
 197          const claudeResponse = await browser.runtime.sendMessage({
 198            type: 'VAULT_GET_CREDENTIAL',
 199            payload: { type: 'claude' },
 200          });
 201          if (claudeResponse.success && claudeResponse.credential) {
 202            const creds = claudeResponse.credential;
 203            // Set the API key in the background service
 204            if (creds.apiKey) {
 205              await browser.runtime.sendMessage({
 206                type: 'SET_CLAUDE_API_KEY',
 207                payload: { apiKey: creds.apiKey },
 208              });
 209              claudeEnabled = true;
 210              claudeAutoSuggest = creds.autoSuggest || false;
 211            }
 212          }
 213        } catch (e) {
 214          console.warn('[Mnemonic] Failed to load Claude credentials from vault:', e);
 215        }
 216      }
 217  
 218      // Load Todoist credentials from vault
 219      // Note: Save directly to settings instead of re-validating via API call.
 220      // authenticateWithApiToken() makes an external fetch to api.todoist.com which
 221      // can fail transiently (network, rate-limit, DNS on fresh boot), causing the
 222      // token to silently not restore. The token was already validated when first stored.
 223      if (vaultCredentials.todoist) {
 224        try {
 225          const todoistResponse = await browser.runtime.sendMessage({
 226            type: 'VAULT_GET_CREDENTIAL',
 227            payload: { type: 'todoist' },
 228          });
 229          if (todoistResponse.success && todoistResponse.credential) {
 230            const creds = todoistResponse.credential;
 231            if (creds.accessToken) {
 232              // Restore settings directly — skip API re-validation
 233              await todoistAuth.saveSettings({
 234                enabled: true,
 235                authMethod: creds.authMethod || 'api_token',
 236                accessToken: creds.accessToken,
 237                masterProjectId: creds.masterProjectId,
 238              });
 239              todoistEnabled = true;
 240              todoistConnection = await todoistClient.initializeFromStorage();
 241            }
 242          }
 243        } catch (e) {
 244          console.warn('[Mnemonic] Failed to load Todoist credentials from vault:', e);
 245        }
 246      }
 247  
 248      // Load Logseq credentials from vault
 249      if (vaultCredentials.logseq) {
 250        try {
 251          const logseqResponse = await browser.runtime.sendMessage({
 252            type: 'VAULT_GET_CREDENTIAL',
 253            payload: { type: 'logseq' },
 254          });
 255          if (logseqResponse.success && logseqResponse.credential) {
 256            const creds = logseqResponse.credential;
 257            if (creds.authToken) {
 258              logseqSettings = {
 259                ...logseqSettings,
 260                enabled: true,
 261                apiUrl: creds.apiUrl || logseqSettings.apiUrl,
 262                authToken: creds.authToken,
 263                autoSync: creds.autoSync ?? logseqSettings.autoSync,
 264                syncInterval: creds.syncInterval ?? logseqSettings.syncInterval,
 265              };
 266              logseqClient.setSettings({ ...logseqSettings });
 267              await browser.storage.local.set({ logseqSettings: { ...logseqSettings } });
 268  
 269              // Check connection
 270              const state = await logseqClient.checkConnection();
 271              logseqConnected = state.available;
 272              logseqGraphName = state.graphName;
 273            }
 274          }
 275        } catch (e) {
 276          console.warn('[Mnemonic] Failed to load Logseq credentials from vault:', e);
 277        }
 278      }
 279    }
 280  
 281    async function handleSelectDirectory() {
 282      selectingDirectory = true;
 283      filesystemError = null;
 284      try {
 285        const success = await filesystemBridge.selectDirectory();
 286        if (success) {
 287          filesystemConnected = true;
 288          const state = filesystemBridge.getState();
 289          filesystemDirectory = state.configDir || null;
 290          // Notify background script of filesystem connection
 291          await browser.runtime.sendMessage({
 292            type: 'INIT_SYNC_BACKEND',
 293            payload: { backendType: 'filesystem' },
 294          });
 295        }
 296      } catch (e) {
 297        filesystemError = e instanceof Error ? e.message : 'Failed to select directory';
 298      } finally {
 299        selectingDirectory = false;
 300      }
 301    }
 302  
 303    async function handleSyncBackendChange() {
 304      // Re-initialize sync when backend changes
 305      if (settings.syncBackend === 'filesystem' && filesystemSupported) {
 306        const connected = await filesystemBridge.connect();
 307        filesystemConnected = connected;
 308        if (connected) {
 309          const state = filesystemBridge.getState();
 310          filesystemDirectory = state.configDir || null;
 311        }
 312      } else {
 313        filesystemConnected = false;
 314        filesystemDirectory = null;
 315      }
 316  
 317      // Check native connection when switching to native
 318      if (settings.syncBackend === 'native') {
 319        await checkNativeConnection();
 320      } else {
 321        nativeConnected = false;
 322        nativeConfigDir = null;
 323        nativeError = null;
 324      }
 325    }
 326  
 327    async function checkNativeConnection() {
 328      nativeTesting = true;
 329      nativeError = null;
 330      try {
 331        // First initialize the sync backend
 332        await browser.runtime.sendMessage({
 333          type: 'INIT_SYNC_BACKEND',
 334          payload: { backendType: 'native' },
 335        });
 336  
 337        // Then test the connection
 338        const result = await browser.runtime.sendMessage({ type: 'TEST_SYNC_CONNECTION' });
 339        if (result.success) {
 340          nativeConnected = true;
 341          nativeConfigDir = result.info?.configDir || null;
 342          // Get sync state for last sync time
 343          const state = await browser.runtime.sendMessage({ type: 'GET_SYNC_STATE' });
 344          nativeLastSync = state.state?.lastSyncTime || null;
 345        } else {
 346          nativeConnected = false;
 347          nativeError = result.error || 'Failed to connect to native host';
 348        }
 349      } catch (e) {
 350        nativeConnected = false;
 351        nativeError = e instanceof Error ? e.message : 'Connection test failed';
 352      } finally {
 353        nativeTesting = false;
 354      }
 355    }
 356  
 357    async function handleSyncNow() {
 358      nativeSyncing = true;
 359      nativeError = null;
 360      try {
 361        const result = await browser.runtime.sendMessage({ type: 'SYNC_NOW' });
 362        if (result.success) {
 363          nativeLastSync = new Date().toISOString();
 364        } else {
 365          nativeError = result.error || 'Sync failed';
 366        }
 367      } catch (e) {
 368        nativeError = e instanceof Error ? e.message : 'Sync failed';
 369      } finally {
 370        nativeSyncing = false;
 371      }
 372    }
 373  
 374    function formatLastSync(isoString: string | null): string {
 375      if (!isoString) return 'Never';
 376      try {
 377        const date = new Date(isoString);
 378        const now = new Date();
 379        const diffMs = now.getTime() - date.getTime();
 380        const diffMins = Math.floor(diffMs / 60000);
 381        const diffHours = Math.floor(diffMs / 3600000);
 382  
 383        if (diffMins < 1) return 'Just now';
 384        if (diffMins < 60) return `${diffMins} min ago`;
 385        if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
 386        return date.toLocaleDateString();
 387      } catch {
 388        return 'Unknown';
 389      }
 390    }
 391  
 392    async function saveSettings() {
 393      saving = true;
 394      try {
 395        const result = await browser.storage.local.get('workspaceConfig');
 396        const config = result.workspaceConfig || {};
 397        // Create plain object copy to avoid Firefox DataCloneError with Svelte 5 proxies
 398        config.settings = { ...settings };
 399        config.metadata = {
 400          ...config.metadata,
 401          lastModified: new Date().toISOString(),
 402        };
 403        await browser.storage.local.set({ workspaceConfig: config });
 404        saved = true;
 405        setTimeout(() => (saved = false), 2000);
 406      } catch (e) {
 407        console.error('[Mnemonic] Failed to save settings:', e);
 408      } finally {
 409        saving = false;
 410      }
 411    }
 412  
 413    async function saveClaudeApiKey() {
 414      if (!claudeApiKey.trim()) return;
 415  
 416      try {
 417        await browser.runtime.sendMessage({
 418          type: 'SET_CLAUDE_API_KEY',
 419          payload: { apiKey: claudeApiKey.trim() },
 420        });
 421        claudeEnabled = true;
 422        claudeApiKey = ''; // Clear from UI for security
 423      } catch (e) {
 424        console.error('[Mnemonic] Failed to save Claude API key:', e);
 425      }
 426    }
 427  
 428    async function clearClaudeApiKey() {
 429      try {
 430        await browser.runtime.sendMessage({
 431          type: 'SET_CLAUDE_API_KEY',
 432          payload: { apiKey: null },
 433        });
 434        claudeEnabled = false;
 435        claudeApiKey = '';
 436      } catch (e) {
 437        console.error('[Mnemonic] Failed to clear Claude API key:', e);
 438      }
 439    }
 440  
 441    async function testClaudeConnection() {
 442      claudeTesting = true;
 443      claudeTestResult = null;
 444      claudeTestError = '';
 445  
 446      try {
 447        const response = await browser.runtime.sendMessage({ type: 'TEST_CLAUDE_CONNECTION' });
 448        claudeTestResult = response.success ? 'success' : 'error';
 449        claudeTestError = response.error || '';
 450      } catch {
 451        claudeTestResult = 'error';
 452        claudeTestError = 'Failed to connect to background service';
 453      } finally {
 454        claudeTesting = false;
 455        setTimeout(() => {
 456          claudeTestResult = null;
 457          claudeTestError = '';
 458        }, 5000);
 459      }
 460    }
 461  
 462    async function updateClaudeAutoSuggest() {
 463      try {
 464        await browser.runtime.sendMessage({
 465          type: 'UPDATE_CLAUDE_SETTINGS',
 466          payload: { autoSuggest: claudeAutoSuggest },
 467        });
 468      } catch (e) {
 469        console.error('[Mnemonic] Failed to update Claude settings:', e);
 470      }
 471    }
 472  
 473    // Logseq functions
 474    async function saveLogseqSettings() {
 475      try {
 476        // Create plain object copy to avoid Firefox DataCloneError with Svelte 5 proxies
 477        const settingsToSave = { ...logseqSettings };
 478        await browser.storage.local.set({ logseqSettings: settingsToSave });
 479        logseqClient.setSettings(settingsToSave);
 480  
 481        // Check connection if enabled
 482        if (logseqSettings.enabled) {
 483          const state = await logseqClient.checkConnection();
 484          logseqConnected = state.available;
 485          logseqGraphName = state.graphName;
 486        } else {
 487          logseqConnected = false;
 488          logseqGraphName = null;
 489        }
 490      } catch (e) {
 491        console.error('[Mnemonic] Failed to save Logseq settings:', e);
 492      }
 493    }
 494  
 495    async function testLogseqConnection() {
 496      logseqTesting = true;
 497      logseqTestResult = null;
 498  
 499      try {
 500        // Unwrap proxy before passing to client
 501        logseqClient.setSettings({ ...logseqSettings });
 502        const state = await logseqClient.checkConnection();
 503        logseqConnected = state.available;
 504        logseqGraphName = state.graphName;
 505        logseqTestResult = state.available ? 'success' : 'error';
 506      } catch {
 507        logseqTestResult = 'error';
 508        logseqConnected = false;
 509        logseqGraphName = null;
 510      } finally {
 511        logseqTesting = false;
 512        setTimeout(() => (logseqTestResult = null), 3000);
 513      }
 514    }
 515  
 516    function toggleLogseqEnabled() {
 517      logseqSettings.enabled = !logseqSettings.enabled;
 518      saveLogseqSettings();
 519    }
 520  
 521    // Graph access functions (for file attachments)
 522    async function requestGraphAccess() {
 523      if (!graphAccessSupported) return;
 524  
 525      graphAccessRequesting = true;
 526      try {
 527        const granted = await graphAccessService.requestAccess();
 528        const state = graphAccessService.getState();
 529        graphAccessGranted = granted;
 530        graphAccessDirectory = state.directoryPath;
 531      } catch (e) {
 532        console.error('[Mnemonic] Failed to request graph access:', e);
 533        graphAccessGranted = false;
 534      } finally {
 535        graphAccessRequesting = false;
 536      }
 537    }
 538  
 539    async function revokeGraphAccess() {
 540      if (!graphAccessSupported) return;
 541  
 542      try {
 543        await graphAccessService.revokeAccess();
 544        graphAccessGranted = false;
 545        graphAccessDirectory = null;
 546      } catch (e) {
 547        console.error('[Mnemonic] Failed to revoke graph access:', e);
 548      }
 549    }
 550  
 551    // Todoist functions
 552    async function saveTodoistApiToken() {
 553      if (!todoistApiToken.trim()) return;
 554  
 555      todoistTesting = true;
 556      todoistTestResult = null;
 557  
 558      try {
 559        const result = await todoistAuth.authenticateWithApiToken(todoistApiToken.trim());
 560  
 561        if (result.success) {
 562          todoistEnabled = true;
 563          todoistConnection = await todoistClient.initializeFromStorage();
 564          todoistApiToken = ''; // Clear from UI for security
 565          todoistTestResult = 'success';
 566        } else {
 567          todoistTestResult = 'error';
 568        }
 569      } catch {
 570        todoistTestResult = 'error';
 571      } finally {
 572        todoistTesting = false;
 573        setTimeout(() => (todoistTestResult = null), 3000);
 574      }
 575    }
 576  
 577    async function testTodoistConnection() {
 578      todoistTesting = true;
 579      todoistTestResult = null;
 580  
 581      try {
 582        todoistConnection = await todoistClient.initializeFromStorage();
 583        todoistTestResult = todoistConnection?.connected ? 'success' : 'error';
 584      } catch {
 585        todoistTestResult = 'error';
 586      } finally {
 587        todoistTesting = false;
 588        setTimeout(() => (todoistTestResult = null), 3000);
 589      }
 590    }
 591  
 592    async function disconnectTodoist() {
 593      await todoistClient.disconnect();
 594      await workspaceSyncService.clearAllMappings();
 595      todoistEnabled = false;
 596      todoistConnection = null;
 597      todoistApiToken = '';
 598    }
 599  
 600    // Credential vault functions
 601    async function handleSetupVault() {
 602      if (vaultNewPassword.length < 8) {
 603        vaultError = 'Password must be at least 8 characters';
 604        return;
 605      }
 606      if (vaultNewPassword !== vaultConfirmPassword) {
 607        vaultError = 'Passwords do not match';
 608        return;
 609      }
 610  
 611      vaultError = null;
 612      vaultSuccess = null;
 613  
 614      try {
 615        const response = await browser.runtime.sendMessage({
 616          type: 'VAULT_SETUP',
 617          payload: { password: vaultNewPassword },
 618        });
 619  
 620        if (response.success) {
 621          vaultSetupMode = false;
 622          vaultNewPassword = '';
 623          vaultConfirmPassword = '';
 624          vaultSuccess = 'Master password created successfully';
 625          await loadVaultStatus();
 626          setTimeout(() => (vaultSuccess = null), 3000);
 627        } else {
 628          vaultError = response.error || 'Failed to setup vault';
 629        }
 630      } catch (e) {
 631        vaultError = e instanceof Error ? e.message : 'Failed to setup vault';
 632      }
 633    }
 634  
 635    async function handleChangeVaultPassword() {
 636      if (vaultNewPassword.length < 8) {
 637        vaultError = 'New password must be at least 8 characters';
 638        return;
 639      }
 640      if (vaultNewPassword !== vaultConfirmPassword) {
 641        vaultError = 'New passwords do not match';
 642        return;
 643      }
 644  
 645      vaultError = null;
 646      vaultSuccess = null;
 647  
 648      try {
 649        const response = await browser.runtime.sendMessage({
 650          type: 'VAULT_CHANGE_PASSWORD',
 651          payload: { oldPassword: vaultOldPassword, newPassword: vaultNewPassword },
 652        });
 653  
 654        if (response.success) {
 655          vaultChangingPassword = false;
 656          vaultOldPassword = '';
 657          vaultNewPassword = '';
 658          vaultConfirmPassword = '';
 659          vaultSuccess = 'Master password changed successfully';
 660          setTimeout(() => (vaultSuccess = null), 3000);
 661        } else {
 662          vaultError = response.error || 'Failed to change password';
 663        }
 664      } catch (e) {
 665        vaultError = e instanceof Error ? e.message : 'Failed to change password';
 666      }
 667    }
 668  
 669    async function handleLockVault() {
 670      try {
 671        await browser.runtime.sendMessage({ type: 'VAULT_LOCK' });
 672        await loadVaultStatus();
 673      } catch (e) {
 674        vaultError = e instanceof Error ? e.message : 'Failed to lock vault';
 675      }
 676    }
 677  
 678    async function handleUnlockVault() {
 679      if (!vaultOldPassword) return;
 680  
 681      vaultError = null;
 682      vaultSuccess = null;
 683  
 684      try {
 685        const response = await browser.runtime.sendMessage({
 686          type: 'VAULT_UNLOCK',
 687          payload: { password: vaultOldPassword },
 688        });
 689  
 690        if (response.success) {
 691          vaultOldPassword = '';
 692          vaultSuccess = 'Vault unlocked - restoring integrations...';
 693          await loadVaultStatus();
 694          // Load and restore credentials from vault
 695          await loadCredentialsFromVault();
 696          vaultSuccess = 'Vault unlocked and integrations restored';
 697          setTimeout(() => (vaultSuccess = null), 3000);
 698        } else {
 699          vaultError = response.error || 'Incorrect password';
 700        }
 701      } catch (e) {
 702        vaultError = e instanceof Error ? e.message : 'Failed to unlock vault';
 703      }
 704    }
 705  
 706    // Pause media sites functions
 707    function startEditPauseMediaSites() {
 708      // Combine default sites and custom sites for editing
 709      const customSites = settings.pauseMediaSites || [];
 710      const allSites = [
 711        '# Default sites (pre-injected for better performance)',
 712        ...DEFAULT_PAUSE_MEDIA_SITES,
 713        '',
 714        '# Your custom sites (add below this line)',
 715        '# Sites with ports (e.g., :3000) work via programmatic injection',
 716        ...customSites,
 717      ];
 718      pauseMediaText = allSites.join('\n');
 719      pauseMediaEditMode = true;
 720    }
 721  
 722    function cancelEditPauseMediaSites() {
 723      pauseMediaEditMode = false;
 724      pauseMediaText = '';
 725    }
 726  
 727    async function savePauseMediaSites() {
 728      // Parse the text and extract only custom sites (not defaults)
 729      const lines = pauseMediaText.split('\n');
 730      const customSites: string[] = [];
 731      let inCustomSection = false;
 732  
 733      for (const line of lines) {
 734        const trimmed = line.trim();
 735  
 736        // Track when we enter the custom section
 737        if (trimmed.includes('Your custom sites') || trimmed.includes('custom sites')) {
 738          inCustomSection = true;
 739          continue;
 740        }
 741  
 742        // Skip empty lines and comments
 743        if (!trimmed || trimmed.startsWith('#')) continue;
 744  
 745        // Skip default sites
 746        if (DEFAULT_PAUSE_MEDIA_SITES.includes(trimmed)) continue;
 747  
 748        // Add to custom sites if we're in custom section or it's not a default
 749        customSites.push(trimmed);
 750      }
 751  
 752      // Update settings
 753      settings.pauseMediaSites = customSites;
 754      await saveSettings();
 755  
 756      pauseMediaEditMode = false;
 757      pauseMediaText = '';
 758      pauseMediaSaved = true;
 759      setTimeout(() => (pauseMediaSaved = false), 2000);
 760    }
 761  
 762    function resetPauseMediaToDefaults() {
 763      settings.pauseMediaSites = [];
 764      saveSettings();
 765      pauseMediaSaved = true;
 766      setTimeout(() => (pauseMediaSaved = false), 2000);
 767    }
 768  </script>
 769  
 770  <div class="options">
 771    <header class="options-header">
 772      <div class="logo-mark">M</div>
 773      <h1 class="options-title">Mnemonic Settings</h1>
 774    </header>
 775  
 776    <main class="options-content">
 777      <section class="settings-section">
 778        <h2 class="section-title">Auto-Save</h2>
 779        <div class="setting-item">
 780          <label class="setting-label">
 781            <input
 782              type="checkbox"
 783              bind:checked={settings.autoSave}
 784              class="checkbox"
 785            />
 786            <span>Automatically save workspace state</span>
 787          </label>
 788          <p class="setting-description">
 789            Periodically save open tabs and window positions
 790          </p>
 791        </div>
 792  
 793        <div class="setting-item">
 794          <label class="setting-label">
 795            <span>Save interval</span>
 796            <select bind:value={settings.autoSaveInterval} class="select" disabled={!settings.autoSave}>
 797              <option value={15000}>15 seconds</option>
 798              <option value={30000}>30 seconds</option>
 799              <option value={60000}>1 minute</option>
 800              <option value={300000}>5 minutes</option>
 801            </select>
 802          </label>
 803        </div>
 804      </section>
 805  
 806      <section class="settings-section">
 807        <h2 class="section-title">Window Management</h2>
 808        <div class="setting-item">
 809          <label class="setting-label">
 810            <input
 811              type="checkbox"
 812              bind:checked={settings.showOrphanPrompt}
 813              class="checkbox"
 814            />
 815            <span>Prompt when new windows are opened</span>
 816          </label>
 817          <p class="setting-description">
 818            Ask whether to add unassigned windows to the current context
 819          </p>
 820        </div>
 821  
 822        <div class="setting-item">
 823          <label class="setting-label">
 824            <input
 825              type="checkbox"
 826              bind:checked={settings.confirmContextSwitch}
 827              class="checkbox"
 828            />
 829            <span>Confirm before context switch</span>
 830          </label>
 831          <p class="setting-description">
 832            Show a confirmation dialog before closing windows during context switch
 833          </p>
 834        </div>
 835  
 836        <div class="setting-item">
 837          <label class="setting-label">
 838            <input
 839              type="checkbox"
 840              bind:checked={settings.restoreWindowPositions}
 841              class="checkbox"
 842            />
 843            <span>Restore window positions</span>
 844          </label>
 845          <p class="setting-description">
 846            Restore window size and position when opening workspaces
 847          </p>
 848        </div>
 849  
 850        <div class="setting-item">
 851          <label class="setting-label">
 852            <input
 853              type="checkbox"
 854              bind:checked={settings.useSmartPositioning}
 855              class="checkbox"
 856              disabled={!settings.restoreWindowPositions}
 857            />
 858            <span>Multi-monitor aware positioning</span>
 859          </label>
 860          <p class="setting-description">
 861            Adapt window positions when monitor configuration changes
 862          </p>
 863        </div>
 864  
 865        <div class="setting-item">
 866          <label class="setting-label">
 867            <input
 868              type="checkbox"
 869              bind:checked={settings.lazyLoadTabs}
 870              class="checkbox"
 871            />
 872            <span>Lazy load tabs when restoring workspaces</span>
 873          </label>
 874          <p class="setting-description">
 875            Only load the active tab initially; other tabs load when clicked. Reduces memory and CPU usage.
 876          </p>
 877        </div>
 878      </section>
 879  
 880      <section class="settings-section">
 881        <h2 class="section-title">Appearance</h2>
 882        <div class="setting-item">
 883          <label class="setting-label" for="theme-select">
 884            <span>Theme</span>
 885            <select id="theme-select" bind:value={settings.theme} class="select">
 886              <option value="system">System default</option>
 887              <option value="light">Light</option>
 888              <option value="dark">Dark</option>
 889            </select>
 890          </label>
 891          <p class="setting-description">
 892            Choose the color theme for the extension
 893          </p>
 894        </div>
 895      </section>
 896  
 897      <section class="settings-section">
 898        <h2 class="section-title">Sync Backend</h2>
 899        <div class="setting-item">
 900          <label class="setting-label">
 901            <span>Synchronization method</span>
 902            <select bind:value={settings.syncBackend} class="select" onchange={handleSyncBackendChange}>
 903              <option value="none">None (local only)</option>
 904              <option value="filesystem" disabled={!filesystemSupported}>
 905                File System {!filesystemSupported ? '(Not Supported)' : ''}
 906              </option>
 907              <option value="native">Native Messaging (Syncthing)</option>
 908            </select>
 909          </label>
 910          <p class="setting-description">
 911            Choose how to sync workspaces across devices
 912          </p>
 913        </div>
 914  
 915        {#if settings.syncBackend === 'filesystem'}
 916          <div class="setting-item filesystem-config">
 917            <div class="filesystem-status">
 918              <span class="status-label">Status:</span>
 919              {#if filesystemConnected}
 920                <span class="status-badge status-active">Connected</span>
 921              {:else}
 922                <span class="status-badge status-inactive">Not Connected</span>
 923              {/if}
 924            </div>
 925  
 926            {#if filesystemDirectory}
 927              <div class="filesystem-folder">
 928                <span class="folder-label">Sync folder:</span>
 929                <span class="folder-path">{filesystemDirectory}</span>
 930              </div>
 931            {/if}
 932  
 933            <div class="filesystem-actions">
 934              <button
 935                type="button"
 936                class="btn-primary"
 937                onclick={handleSelectDirectory}
 938                disabled={selectingDirectory}
 939              >
 940                {selectingDirectory ? 'Selecting...' : filesystemDirectory ? 'Change Directory' : 'Select Directory'}
 941              </button>
 942            </div>
 943  
 944            {#if filesystemError}
 945              <p class="filesystem-error">{filesystemError}</p>
 946            {/if}
 947  
 948            <div class="filesystem-instructions">
 949              <p class="setting-description">
 950                Select a folder to store your workspace data. Use a cloud-synced folder
 951                (Dropbox, Google Drive, OneDrive) to sync across devices.
 952              </p>
 953              <p class="setting-description note">
 954                <strong>Note:</strong> Permission is usually remembered. Re-granting may be needed if you clear browser data.
 955              </p>
 956            </div>
 957          </div>
 958        {/if}
 959  
 960        {#if settings.syncBackend === 'native'}
 961          <div class="setting-item native-config">
 962            <div class="native-status">
 963              <span class="status-label">Status:</span>
 964              {#if nativeTesting}
 965                <span class="status-badge status-testing">Testing...</span>
 966              {:else if nativeConnected}
 967                <span class="status-badge status-active">Connected</span>
 968              {:else}
 969                <span class="status-badge status-inactive">Not Connected</span>
 970              {/if}
 971            </div>
 972  
 973            {#if nativeConnected}
 974              <div class="native-info">
 975                <div class="info-row">
 976                  <span class="info-label">Config Directory:</span>
 977                  <code class="info-value">{nativeConfigDir || 'Unknown'}</code>
 978                </div>
 979                <div class="info-row">
 980                  <span class="info-label">Last Sync:</span>
 981                  <span class="info-value">{formatLastSync(nativeLastSync)}</span>
 982                </div>
 983              </div>
 984  
 985              <div class="native-actions">
 986                <button
 987                  class="btn btn-secondary"
 988                  onclick={checkNativeConnection}
 989                  disabled={nativeTesting}
 990                >
 991                  {nativeTesting ? 'Testing...' : 'Test Connection'}
 992                </button>
 993                <button
 994                  class="btn btn-primary"
 995                  onclick={handleSyncNow}
 996                  disabled={nativeSyncing}
 997                >
 998                  {nativeSyncing ? 'Syncing...' : 'Sync Now'}
 999                </button>
1000              </div>
1001            {:else}
1002              <div class="native-actions">
1003                <button
1004                  class="btn btn-primary"
1005                  onclick={checkNativeConnection}
1006                  disabled={nativeTesting}
1007                >
1008                  {nativeTesting ? 'Connecting...' : 'Connect'}
1009                </button>
1010              </div>
1011            {/if}
1012  
1013            {#if nativeError}
1014              <div class="native-error">
1015                <svg class="error-icon" fill="currentColor" viewBox="0 0 20 20">
1016                  <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
1017                </svg>
1018                <span>{nativeError}</span>
1019              </div>
1020            {/if}
1021  
1022            <div class="native-instructions">
1023              <h4>Setup Instructions:</h4>
1024              <ol>
1025                <li>
1026                  <strong>Install the native host:</strong> Run <code>install_windows.bat</code> (Windows)
1027                  or <code>install_macos.sh</code> (macOS/Linux) from the <code>native-host</code> folder.
1028                </li>
1029                <li>
1030                  <strong>Configure Syncthing folder:</strong> The extension syncs to
1031                  <code>~/Syncthing/MnemonicWorkspaces/</code> by default. Create this folder
1032                  and add it to Syncthing.
1033                </li>
1034                <li>
1035                  <strong>Reload extension:</strong> After installing the native host, reload
1036                  this extension from <code>chrome://extensions</code> or <code>about:debugging</code>.
1037                </li>
1038                <li>
1039                  <strong>Click Connect:</strong> The extension will communicate with the native
1040                  host to read/write the backup file.
1041                </li>
1042              </ol>
1043              <p class="setup-note">
1044                <strong>How it works:</strong> When you click "Sync Now", all your workspaces are
1045                saved to <code>workspaces.json</code> in the Syncthing folder. Syncthing then
1046                syncs this file to your other devices. On other devices, use "Check for Changes"
1047                to import updates.
1048              </p>
1049            </div>
1050          </div>
1051        {/if}
1052      </section>
1053  
1054      <section class="settings-section">
1055        <h2 class="section-title">Claude AI Integration</h2>
1056        <p class="section-description">
1057          Enable AI-powered workspace suggestions using Claude.
1058        </p>
1059  
1060        <div class="setting-item">
1061          <div class="api-key-status">
1062            <span class="status-label">Status:</span>
1063            {#if claudeEnabled}
1064              <span class="status-badge status-active">Configured</span>
1065            {:else}
1066              <span class="status-badge status-inactive">Not configured</span>
1067            {/if}
1068          </div>
1069        </div>
1070  
1071        {#if !claudeEnabled}
1072          <div class="setting-item">
1073            <label class="setting-label" for="claude-api-key">
1074              <span>API Key</span>
1075            </label>
1076            <div class="api-key-input">
1077              <input
1078                id="claude-api-key"
1079                type="password"
1080                bind:value={claudeApiKey}
1081                placeholder="sk-ant-..."
1082                class="input"
1083              />
1084              <button
1085                class="btn-secondary"
1086                onclick={saveClaudeApiKey}
1087                disabled={!claudeApiKey.trim()}
1088              >
1089                Save Key
1090              </button>
1091            </div>
1092            <p class="setting-description">
1093              Get your API key from <a href="https://console.anthropic.com" target="_blank" rel="noopener noreferrer">console.anthropic.com</a>
1094            </p>
1095          </div>
1096        {:else}
1097          <div class="setting-item">
1098            <div class="api-key-actions">
1099              <button
1100                class="btn-secondary"
1101                onclick={testClaudeConnection}
1102                disabled={claudeTesting}
1103              >
1104                {#if claudeTesting}
1105                  Testing...
1106                {:else if claudeTestResult === 'success'}
1107                  Connected!
1108                {:else if claudeTestResult === 'error'}
1109                  Failed
1110                {:else}
1111                  Test Connection
1112                {/if}
1113              </button>
1114              <button class="btn-danger" onclick={clearClaudeApiKey}>
1115                Remove API Key
1116              </button>
1117            </div>
1118            {#if claudeTestResult === 'error' && claudeTestError}
1119              <p class="setting-description" style="color: #EF4444; margin-top: 4px;">
1120                {claudeTestError}
1121              </p>
1122            {/if}
1123          </div>
1124  
1125          <div class="setting-item">
1126            <label class="setting-label">
1127              <input
1128                type="checkbox"
1129                bind:checked={claudeAutoSuggest}
1130                onchange={updateClaudeAutoSuggest}
1131                class="checkbox"
1132              />
1133              <span>Auto-suggest improvements</span>
1134            </label>
1135            <p class="setting-description">
1136              Automatically analyze workspaces and suggest organizational improvements
1137            </p>
1138          </div>
1139        {/if}
1140      </section>
1141  
1142      <section class="settings-section pause-media-section">
1143        <h2 class="section-title">Pause Media Sites</h2>
1144        <p class="section-description">
1145          Configure which sites the "Pause Audio" feature applies to. Default sites include YouTube, Vimeo, Twitch, Netflix, Spotify, and more.
1146        </p>
1147  
1148        <div class="setting-item">
1149          <div class="pause-media-status">
1150            <span class="status-label">Default sites:</span>
1151            <span class="site-count">{DEFAULT_PAUSE_MEDIA_SITES.length} sites</span>
1152            {#if (settings.pauseMediaSites?.length || 0) > 0}
1153              <span class="divider">|</span>
1154              <span class="status-label">Custom sites:</span>
1155              <span class="site-count custom">{settings.pauseMediaSites?.length || 0} sites</span>
1156            {/if}
1157          </div>
1158        </div>
1159  
1160        {#if !pauseMediaEditMode}
1161          <div class="setting-item">
1162            <div class="pause-media-preview">
1163              <div class="sites-list">
1164                <strong>Default sites:</strong>
1165                <span class="sites-preview">YouTube, Vimeo, Twitch, Netflix, Hulu, Disney+, Prime Video, HBO Max, Spotify, SoundCloud, Bandcamp, Tidal, Deezer, Pandora, Invidious (public instances)</span>
1166              </div>
1167              {#if (settings.pauseMediaSites?.length || 0) > 0}
1168                <div class="sites-list custom-list">
1169                  <strong>Custom sites:</strong>
1170                  <span class="sites-preview">{settings.pauseMediaSites?.join(', ')}</span>
1171                </div>
1172              {/if}
1173            </div>
1174          </div>
1175  
1176          <div class="setting-item">
1177            <div class="pause-media-actions">
1178              <button class="btn-secondary" onclick={startEditPauseMediaSites}>
1179                Edit Sites
1180              </button>
1181              {#if (settings.pauseMediaSites?.length || 0) > 0}
1182                <button class="btn-danger-outline" onclick={resetPauseMediaToDefaults}>
1183                  Reset to Defaults
1184                </button>
1185              {/if}
1186              {#if pauseMediaSaved}
1187                <span class="save-indicator">Saved!</span>
1188              {/if}
1189            </div>
1190          </div>
1191        {:else}
1192          <div class="setting-item pause-media-editor">
1193            <label class="setting-label" for="pause-media-textarea">
1194              <span>Edit site patterns</span>
1195            </label>
1196            <textarea
1197              id="pause-media-textarea"
1198              class="sites-textarea"
1199              bind:value={pauseMediaText}
1200              rows="20"
1201              placeholder="Enter site patterns, one per line..."
1202            ></textarea>
1203            <p class="setting-description">
1204              Use match patterns like <code>*://*.example.com/*</code> or <code>*://localhost:8096/*</code>
1205              <br />
1206              Lines starting with <code>#</code> are comments. Custom sites are saved separately from defaults.
1207            </p>
1208            <div class="pause-media-actions">
1209              <button class="btn-primary" onclick={savePauseMediaSites}>
1210                Save Changes
1211              </button>
1212              <button class="btn-secondary" onclick={cancelEditPauseMediaSites}>
1213                Cancel
1214              </button>
1215            </div>
1216          </div>
1217        {/if}
1218  
1219        <div class="setting-item pause-media-info">
1220          <p class="setting-description note">
1221            <strong>Note:</strong> Default sites use pre-injected content scripts for instant response.
1222            Custom sites (especially those with ports like <code>:3000</code>) work via programmatic injection when you click "Pause Audio".
1223          </p>
1224        </div>
1225      </section>
1226  
1227      <section class="settings-section">
1228        <h2 class="section-title">Logseq Integration</h2>
1229        <p class="section-description">
1230          Sync workspace notes with Logseq for a connected knowledge graph.
1231        </p>
1232  
1233        <div class="setting-item">
1234          <label class="setting-label">
1235            <input
1236              type="checkbox"
1237              checked={logseqSettings.enabled}
1238              onchange={toggleLogseqEnabled}
1239              class="checkbox"
1240            />
1241            <span>Enable Logseq Notes</span>
1242          </label>
1243          <p class="setting-description">
1244            Store workspace notes in Logseq. Requires Logseq to be running with HTTP API enabled.
1245          </p>
1246        </div>
1247  
1248        {#if logseqSettings.enabled}
1249          <div class="setting-item">
1250            <div class="api-key-status">
1251              <span class="status-label">Status:</span>
1252              {#if logseqConnected}
1253                <span class="status-badge status-active">Connected to {logseqGraphName}</span>
1254              {:else}
1255                <span class="status-badge status-inactive">Not connected</span>
1256              {/if}
1257            </div>
1258          </div>
1259  
1260          <div class="setting-item">
1261            <label class="setting-label" for="logseq-api-url">
1262              <span>API URL</span>
1263            </label>
1264            <div class="api-key-input">
1265              <input
1266                id="logseq-api-url"
1267                type="text"
1268                bind:value={logseqSettings.apiUrl}
1269                placeholder="http://127.0.0.1:12315/api"
1270                class="input"
1271              />
1272              <button class="btn-secondary" onclick={saveLogseqSettings}>
1273                Save
1274              </button>
1275            </div>
1276            <p class="setting-description">
1277              Default: http://127.0.0.1:12315/api (Logseq's default HTTP API port)
1278            </p>
1279          </div>
1280  
1281          <div class="setting-item">
1282            <label class="setting-label" for="logseq-token">
1283              <span>Auth Token (optional)</span>
1284            </label>
1285            <div class="api-key-input">
1286              <input
1287                id="logseq-token"
1288                type="password"
1289                bind:value={logseqSettings.authToken}
1290                placeholder="Leave empty if not using authentication"
1291                class="input"
1292              />
1293              <button class="btn-secondary" onclick={saveLogseqSettings}>
1294                Save
1295              </button>
1296            </div>
1297            <p class="setting-description">
1298              Only needed if you've configured Logseq API authentication.
1299            </p>
1300          </div>
1301  
1302          <div class="setting-item">
1303            <div class="api-key-actions">
1304              <button
1305                class="btn-secondary"
1306                onclick={testLogseqConnection}
1307                disabled={logseqTesting}
1308              >
1309                {#if logseqTesting}
1310                  Testing...
1311                {:else if logseqTestResult === 'success'}
1312                  Connected!
1313                {:else if logseqTestResult === 'error'}
1314                  Failed
1315                {:else}
1316                  Test Connection
1317                {/if}
1318              </button>
1319            </div>
1320          </div>
1321  
1322          <div class="setting-item">
1323            <label class="setting-label">
1324              <input
1325                type="checkbox"
1326                bind:checked={logseqSettings.autoSync}
1327                onchange={saveLogseqSettings}
1328                class="checkbox"
1329              />
1330              <span>Auto-sync notes</span>
1331            </label>
1332            <p class="setting-description">
1333              Automatically sync notes with Logseq when changes are made
1334            </p>
1335          </div>
1336  
1337          <!-- Graph Access for File Attachments -->
1338          {#if graphAccessSupported}
1339            <div class="setting-item graph-access-section">
1340              <h4 class="subsection-title">Graph Access for Attachments</h4>
1341              <p class="setting-description">
1342                Grant access to your Logseq graph folder to store file attachments directly.
1343                This allows you to drag and drop files into workspace resources.
1344              </p>
1345  
1346              <div class="graph-access-status">
1347                <div class="api-key-status">
1348                  <span class="status-label">Status:</span>
1349                  {#if graphAccessGranted}
1350                    <span class="status-badge status-active">Access granted</span>
1351                  {:else}
1352                    <span class="status-badge status-inactive">No access</span>
1353                  {/if}
1354                </div>
1355  
1356                {#if graphAccessGranted && graphAccessDirectory}
1357                  <div class="graph-directory-info">
1358                    <span class="directory-label">Directory:</span>
1359                    <span class="directory-path">{graphAccessDirectory}</span>
1360                  </div>
1361                {/if}
1362              </div>
1363  
1364              <div class="api-key-actions">
1365                {#if graphAccessGranted}
1366                  <button
1367                    class="btn-secondary btn-danger"
1368                    onclick={revokeGraphAccess}
1369                  >
1370                    Revoke Access
1371                  </button>
1372                {:else}
1373                  <button
1374                    class="btn-primary"
1375                    onclick={requestGraphAccess}
1376                    disabled={graphAccessRequesting}
1377                  >
1378                    {#if graphAccessRequesting}
1379                      Requesting...
1380                    {:else}
1381                      Grant Access
1382                    {/if}
1383                  </button>
1384                {/if}
1385              </div>
1386  
1387              <p class="setting-description">
1388                When you click "Grant Access", select your Logseq graph root folder.
1389                Files will be stored in <code>assets/mnemonic/</code> within your graph.
1390              </p>
1391            </div>
1392          {:else}
1393            <div class="setting-item graph-access-section">
1394              <h4 class="subsection-title">Graph Access for Attachments</h4>
1395              <p class="setting-description warning-text">
1396                File attachment storage is not supported in this browser.
1397                Use Chrome, Edge, or another Chromium-based browser for full attachment support.
1398              </p>
1399            </div>
1400          {/if}
1401        {/if}
1402  
1403        <div class="setting-item logseq-instructions">
1404          <h4>How to enable Logseq HTTP API:</h4>
1405          <ol>
1406            <li>Open Logseq</li>
1407            <li>Go to Settings (gear icon)</li>
1408            <li>Enable "HTTP APIs server" under Features</li>
1409            <li>Restart Logseq</li>
1410            <li>The API will be available at http://127.0.0.1:12315/api</li>
1411          </ol>
1412        </div>
1413      </section>
1414  
1415      <section class="settings-section">
1416        <h2 class="section-title">Todoist Integration</h2>
1417        <p class="section-description">
1418          Sync workspace tasks with Todoist for cross-platform task management.
1419        </p>
1420  
1421        <div class="setting-item">
1422          <div class="api-key-status">
1423            <span class="status-label">Status:</span>
1424            {#if todoistConnection?.connected}
1425              <span class="status-badge status-active">Connected</span>
1426            {:else if todoistEnabled}
1427              <span class="status-badge status-inactive">Disconnected</span>
1428            {:else}
1429              <span class="status-badge status-inactive">Not configured</span>
1430            {/if}
1431          </div>
1432        </div>
1433  
1434        {#if todoistConnection?.connected}
1435          <div class="setting-item">
1436            <div class="todoist-info">
1437              <div class="info-row">
1438                <span class="info-label">Master Project:</span>
1439                <span class="info-value">{todoistConnection.masterProjectName}</span>
1440              </div>
1441              {#if todoistConnection.lastSync}
1442                <div class="info-row">
1443                  <span class="info-label">Last Sync:</span>
1444                  <span class="info-value">{formatLastSync(todoistConnection.lastSync)}</span>
1445                </div>
1446              {/if}
1447              <div class="info-row">
1448                <span class="info-label">Auth Method:</span>
1449                <span class="info-value">{todoistConnection.authMethod === 'oauth' ? 'OAuth' : 'API Token'}</span>
1450              </div>
1451            </div>
1452          </div>
1453  
1454          <div class="setting-item">
1455            <div class="api-key-actions">
1456              <button
1457                class="btn-secondary"
1458                onclick={testTodoistConnection}
1459                disabled={todoistTesting}
1460              >
1461                {#if todoistTesting}
1462                  Testing...
1463                {:else if todoistTestResult === 'success'}
1464                  Connected!
1465                {:else if todoistTestResult === 'error'}
1466                  Failed
1467                {:else}
1468                  Test Connection
1469                {/if}
1470              </button>
1471              <button class="btn-danger" onclick={disconnectTodoist}>
1472                Disconnect
1473              </button>
1474            </div>
1475          </div>
1476        {:else if todoistEnabled}
1477          <div class="setting-item">
1478            <p class="setting-description" style="margin-left: 0; color: var(--text-secondary);">
1479              API token is saved but connection failed{#if todoistConnection?.error}: {todoistConnection.error}{/if}
1480            </p>
1481            <div class="api-key-actions">
1482              <button
1483                class="btn-secondary"
1484                onclick={testTodoistConnection}
1485                disabled={todoistTesting}
1486              >
1487                {#if todoistTesting}
1488                  Reconnecting...
1489                {:else if todoistTestResult === 'success'}
1490                  Connected!
1491                {:else if todoistTestResult === 'error'}
1492                  Failed
1493                {:else}
1494                  Retry Connection
1495                {/if}
1496              </button>
1497              <button class="btn-danger" onclick={disconnectTodoist}>
1498                Disconnect
1499              </button>
1500            </div>
1501          </div>
1502        {:else}
1503          <div class="setting-item">
1504            <label class="setting-label" for="todoist-api-token">
1505              <span>API Token</span>
1506            </label>
1507            <div class="api-key-input">
1508              <input
1509                id="todoist-api-token"
1510                type="password"
1511                bind:value={todoistApiToken}
1512                placeholder="Enter your Todoist API token"
1513                class="input"
1514              />
1515              <button
1516                class="btn-secondary"
1517                onclick={saveTodoistApiToken}
1518                disabled={!todoistApiToken.trim() || todoistTesting}
1519              >
1520                {#if todoistTesting}
1521                  Connecting...
1522                {:else if todoistTestResult === 'success'}
1523                  Connected!
1524                {:else if todoistTestResult === 'error'}
1525                  Failed
1526                {:else}
1527                  Connect
1528                {/if}
1529              </button>
1530            </div>
1531            <p class="setting-description">
1532              Get your API token from <a href="https://todoist.com/app/settings/integrations/developer" target="_blank" rel="noopener noreferrer">Todoist Settings &gt; Integrations &gt; Developer</a>
1533            </p>
1534          </div>
1535        {/if}
1536  
1537        <div class="setting-item todoist-instructions">
1538          <h4>How it works:</h4>
1539          <ol>
1540            <li>A "Mnemonic" project is created in your Todoist account</li>
1541            <li>Each workspace becomes a sub-project under Mnemonic</li>
1542            <li>Child workspaces become sub-projects of their parent</li>
1543            <li>Tasks sync automatically between the extension and Todoist</li>
1544          </ol>
1545          <p class="setup-note">
1546            <strong>Tip:</strong> Tasks created in Todoist will appear in the workspace dashboard.
1547            Use Todoist's mobile app to manage tasks on the go!
1548          </p>
1549        </div>
1550      </section>
1551  
1552      <!-- Security Section -->
1553      <section class="settings-section">
1554        <h2 class="section-title">
1555          Security
1556          {#if vaultUnlocked}
1557            <span class="status-badge success">Unlocked</span>
1558          {:else if vaultExists}
1559            <span class="status-badge warning">Locked</span>
1560          {/if}
1561        </h2>
1562  
1563        <div class="setting-item">
1564          <p class="setting-description" style="margin-left: 0; margin-bottom: 1rem;">
1565            Encrypt your integration credentials (API keys) with a master password.
1566            Encrypted credentials sync securely across devices via the sync daemon.
1567          </p>
1568  
1569          {#if vaultSuccess}
1570            <div class="success-message">
1571              <svg viewBox="0 0 20 20" fill="currentColor">
1572                <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
1573              </svg>
1574              <span>{vaultSuccess}</span>
1575            </div>
1576          {/if}
1577  
1578          {#if vaultError}
1579            <div class="error-message">
1580              <svg viewBox="0 0 20 20" fill="currentColor">
1581                <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
1582              </svg>
1583              <span>{vaultError}</span>
1584            </div>
1585          {/if}
1586        </div>
1587  
1588        {#if !nativeConnected && settings.syncBackend !== 'native'}
1589          <div class="setting-item">
1590            <div class="warning-box">
1591              <svg viewBox="0 0 20 20" fill="currentColor">
1592                <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
1593              </svg>
1594              <span>Enable Native Messaging Sync to use encrypted credential storage.</span>
1595            </div>
1596          </div>
1597        {:else if !vaultExists && !vaultSetupMode}
1598          <!-- No vault exists - show setup prompt -->
1599          <div class="setting-item">
1600            <div class="vault-setup-prompt">
1601              <div class="shield-icon">
1602                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1603                  <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
1604                  <path d="M9 12l2 2 4-4"/>
1605                </svg>
1606              </div>
1607              <div class="vault-prompt-text">
1608                <strong>Secure your integration credentials</strong>
1609                <p>Create a master password to encrypt API keys for Claude, Todoist, and Logseq.</p>
1610              </div>
1611              <button class="btn-primary" onclick={() => (vaultSetupMode = true)}>
1612                Setup Encryption
1613              </button>
1614            </div>
1615          </div>
1616        {:else if vaultSetupMode}
1617          <!-- Setting up new vault -->
1618          <div class="setting-item">
1619            <div class="vault-setup-form">
1620              <div class="input-group">
1621                <label for="vault-new-password">Master Password</label>
1622                <input
1623                  id="vault-new-password"
1624                  type="password"
1625                  bind:value={vaultNewPassword}
1626                  placeholder="Create a strong password"
1627                  class="text-input"
1628                />
1629              </div>
1630              <div class="input-group">
1631                <label for="vault-confirm-password">Confirm Password</label>
1632                <input
1633                  id="vault-confirm-password"
1634                  type="password"
1635                  bind:value={vaultConfirmPassword}
1636                  placeholder="Confirm your password"
1637                  class="text-input"
1638                />
1639              </div>
1640              <div class="warning-box">
1641                <svg viewBox="0 0 20 20" fill="currentColor">
1642                  <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
1643                </svg>
1644                <span>This password cannot be recovered. Keep it safe!</span>
1645              </div>
1646              <div class="button-row">
1647                <button class="btn-secondary" onclick={() => { vaultSetupMode = false; vaultNewPassword = ''; vaultConfirmPassword = ''; vaultError = null; }}>
1648                  Cancel
1649                </button>
1650                <button
1651                  class="btn-primary"
1652                  onclick={handleSetupVault}
1653                  disabled={!vaultNewPassword || !vaultConfirmPassword || vaultNewPassword !== vaultConfirmPassword}
1654                >
1655                  Create Master Password
1656                </button>
1657              </div>
1658            </div>
1659          </div>
1660        {:else if vaultExists && vaultUnlocked && !vaultChangingPassword}
1661          <!-- Vault is unlocked - show status and actions -->
1662          <div class="setting-item">
1663            <div class="vault-status">
1664              <div class="vault-status-header">
1665                <div class="vault-icon unlocked">
1666                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1667                    <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
1668                    <path d="M7 11V7a5 5 0 0 1 9.9-1"/>
1669                  </svg>
1670                </div>
1671                <div class="vault-status-text">
1672                  <strong>Credential Vault Unlocked</strong>
1673                  <p>Your integration credentials are encrypted and synced.</p>
1674                </div>
1675              </div>
1676              <div class="vault-credentials-list">
1677                <span class="cred-item {vaultCredentials.claude ? 'stored' : 'empty'}">
1678                  Claude {vaultCredentials.claude ? '(stored)' : '(not set)'}
1679                </span>
1680                <span class="cred-item {vaultCredentials.todoist ? 'stored' : 'empty'}">
1681                  Todoist {vaultCredentials.todoist ? '(stored)' : '(not set)'}
1682                </span>
1683                <span class="cred-item {vaultCredentials.logseq ? 'stored' : 'empty'}">
1684                  Logseq {vaultCredentials.logseq ? '(stored)' : '(not set)'}
1685                </span>
1686              </div>
1687              <div class="vault-actions">
1688                <button class="btn-secondary" onclick={() => (vaultChangingPassword = true)}>
1689                  Change Password
1690                </button>
1691                <button class="btn-secondary" onclick={handleLockVault}>
1692                  Lock Vault
1693                </button>
1694              </div>
1695            </div>
1696          </div>
1697        {:else if vaultExists && !vaultUnlocked}
1698          <!-- Vault exists but locked -->
1699          <div class="setting-item">
1700            <div class="vault-locked">
1701              <div class="vault-icon locked">
1702                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1703                  <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
1704                  <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
1705                </svg>
1706              </div>
1707              <div class="vault-locked-text">
1708                <strong>Credential Vault Locked</strong>
1709                <p>Enter your master password to unlock and access your encrypted credentials.</p>
1710              </div>
1711              <div class="input-group">
1712                <input
1713                  type="password"
1714                  bind:value={vaultOldPassword}
1715                  placeholder="Enter master password"
1716                  class="text-input"
1717                  onkeydown={(e) => e.key === 'Enter' && handleUnlockVault()}
1718                />
1719                <button class="btn-primary" onclick={handleUnlockVault} disabled={!vaultOldPassword}>
1720                  Unlock
1721                </button>
1722              </div>
1723            </div>
1724          </div>
1725        {:else if vaultChangingPassword}
1726          <!-- Changing password -->
1727          <div class="setting-item">
1728            <div class="vault-setup-form">
1729              <h4>Change Master Password</h4>
1730              <div class="input-group">
1731                <label for="vault-old-password">Current Password</label>
1732                <input
1733                  id="vault-old-password"
1734                  type="password"
1735                  bind:value={vaultOldPassword}
1736                  placeholder="Enter current password"
1737                  class="text-input"
1738                />
1739              </div>
1740              <div class="input-group">
1741                <label for="vault-new-password-change">New Password</label>
1742                <input
1743                  id="vault-new-password-change"
1744                  type="password"
1745                  bind:value={vaultNewPassword}
1746                  placeholder="Enter new password"
1747                  class="text-input"
1748                />
1749              </div>
1750              <div class="input-group">
1751                <label for="vault-confirm-password-change">Confirm New Password</label>
1752                <input
1753                  id="vault-confirm-password-change"
1754                  type="password"
1755                  bind:value={vaultConfirmPassword}
1756                  placeholder="Confirm new password"
1757                  class="text-input"
1758                />
1759              </div>
1760              <div class="button-row">
1761                <button class="btn-secondary" onclick={() => { vaultChangingPassword = false; vaultOldPassword = ''; vaultNewPassword = ''; vaultConfirmPassword = ''; vaultError = null; }}>
1762                  Cancel
1763                </button>
1764                <button
1765                  class="btn-primary"
1766                  onclick={handleChangeVaultPassword}
1767                  disabled={!vaultOldPassword || !vaultNewPassword || vaultNewPassword !== vaultConfirmPassword}
1768                >
1769                  Change Password
1770                </button>
1771              </div>
1772            </div>
1773          </div>
1774        {/if}
1775  
1776        <div class="setting-item security-info">
1777          <h4>How credential encryption works:</h4>
1778          <ul>
1779            <li>Your master password is never stored - only you know it</li>
1780            <li>A 256-bit encryption key is derived using PBKDF2</li>
1781            <li>Credentials are encrypted with AES-256-GCM</li>
1782            <li>Encrypted data syncs via daemon - decryptable only with your password</li>
1783            <li>On new devices, just enter your master password to restore credentials</li>
1784          </ul>
1785        </div>
1786      </section>
1787    </main>
1788  
1789    <footer class="options-footer">
1790      <button class="btn-primary" onclick={saveSettings} disabled={saving}>
1791        {#if saving}
1792          Saving...
1793        {:else if saved}
1794          Saved!
1795        {:else}
1796          Save Settings
1797        {/if}
1798      </button>
1799    </footer>
1800  </div>
1801  
1802  <style>
1803    .options {
1804      max-width: 960px;
1805      margin: 0 auto;
1806      padding: 2rem 3rem;
1807      min-height: 100vh;
1808      display: flex;
1809      flex-direction: column;
1810    }
1811  
1812    .options-header {
1813      display: flex;
1814      align-items: center;
1815      gap: 1rem;
1816      margin-bottom: 2.5rem;
1817      padding-bottom: 1.5rem;
1818      border-bottom: 1px solid rgba(217, 137, 46, 0.2);
1819    }
1820  
1821    .logo-mark {
1822      width: 48px;
1823      height: 48px;
1824      display: flex;
1825      align-items: center;
1826      justify-content: center;
1827      background-color: var(--color-phosphor);
1828      color: var(--color-bg);
1829      font-weight: bold;
1830      font-size: 1.5rem;
1831      border-radius: 10px;
1832    }
1833  
1834    .options-title {
1835      font-size: 1.75rem;
1836      color: var(--color-phosphor);
1837      margin: 0;
1838    }
1839  
1840    .options-content {
1841      flex: 1;
1842      display: grid;
1843      grid-template-columns: repeat(2, 1fr);
1844      gap: 1.5rem;
1845    }
1846  
1847    /* Make Claude AI and Logseq sections span full width */
1848    .options-content .settings-section:nth-last-child(-n+2) {
1849      grid-column: 1 / -1;
1850    }
1851  
1852    .settings-section {
1853      padding: 1.5rem;
1854      background-color: var(--color-bg-light);
1855      border-radius: 8px;
1856      border: 1px solid rgba(217, 137, 46, 0.15);
1857      height: fit-content;
1858    }
1859  
1860    .section-title {
1861      font-size: 1.125rem;
1862      color: var(--color-phosphor);
1863      margin: 0 0 1rem 0;
1864      padding-bottom: 0.5rem;
1865      border-bottom: 1px solid rgba(217, 137, 46, 0.2);
1866    }
1867  
1868    .setting-item {
1869      margin-bottom: 1.25rem;
1870    }
1871  
1872    .setting-item:last-child {
1873      margin-bottom: 0;
1874    }
1875  
1876    .setting-label {
1877      display: flex;
1878      align-items: center;
1879      gap: 0.75rem;
1880      color: var(--color-text-primary);
1881      cursor: pointer;
1882    }
1883  
1884    .setting-description {
1885      font-size: 0.875rem;
1886      color: var(--color-text-muted);
1887      margin: 0.25rem 0 0 1.75rem;
1888    }
1889  
1890    .checkbox {
1891      width: 18px;
1892      height: 18px;
1893      accent-color: var(--color-phosphor);
1894    }
1895  
1896    .select {
1897      padding: 0.5rem 0.75rem;
1898      background-color: var(--color-bg-dark);
1899      color: var(--color-text-primary);
1900      border: 1px solid rgba(217, 137, 46, 0.3);
1901      border-radius: 4px;
1902      cursor: pointer;
1903    }
1904  
1905    .select:focus {
1906      outline: none;
1907      border-color: var(--color-phosphor);
1908    }
1909  
1910    .select:disabled {
1911      opacity: 0.5;
1912      cursor: not-allowed;
1913    }
1914  
1915    .options-footer {
1916      margin-top: 2rem;
1917      padding-top: 1.5rem;
1918      border-top: 1px solid rgba(217, 137, 46, 0.2);
1919      display: flex;
1920      justify-content: flex-end;
1921    }
1922  
1923    .btn-primary {
1924      min-width: 200px;
1925      padding: 0.875rem 2rem;
1926      background-color: var(--color-phosphor);
1927      color: var(--color-bg);
1928      border: none;
1929      border-radius: 6px;
1930      font-weight: 600;
1931      font-size: 1rem;
1932      cursor: pointer;
1933      transition: background-color 0.2s;
1934    }
1935  
1936    .btn-primary:hover:not(:disabled) {
1937      background-color: var(--color-phosphor-light);
1938    }
1939  
1940    .btn-primary:disabled {
1941      opacity: 0.7;
1942      cursor: not-allowed;
1943    }
1944  
1945    .section-description {
1946      font-size: 0.875rem;
1947      color: var(--color-text-secondary);
1948      margin-bottom: 1rem;
1949    }
1950  
1951    .api-key-status {
1952      display: flex;
1953      align-items: center;
1954      gap: 0.5rem;
1955    }
1956  
1957    .status-label {
1958      color: var(--color-text-secondary);
1959      font-size: 0.875rem;
1960    }
1961  
1962    .status-badge {
1963      padding: 0.25rem 0.5rem;
1964      border-radius: 4px;
1965      font-size: 0.75rem;
1966      font-weight: 500;
1967      text-transform: uppercase;
1968      letter-spacing: 0.05em;
1969    }
1970  
1971    .status-active {
1972      background: rgba(46, 160, 67, 0.2);
1973      color: #2ea043;
1974      border: 1px solid rgba(46, 160, 67, 0.3);
1975    }
1976  
1977    .status-inactive {
1978      background: rgba(217, 137, 46, 0.1);
1979      color: var(--color-text-muted);
1980      border: 1px solid rgba(217, 137, 46, 0.2);
1981    }
1982  
1983    .api-key-input {
1984      display: flex;
1985      gap: 0.5rem;
1986      margin-top: 0.5rem;
1987    }
1988  
1989    .input {
1990      flex: 1;
1991      padding: 0.5rem 0.75rem;
1992      background-color: var(--color-bg-dark);
1993      color: var(--color-text-primary);
1994      border: 1px solid rgba(217, 137, 46, 0.3);
1995      border-radius: 4px;
1996      font-family: var(--font-mono);
1997    }
1998  
1999    .input:focus {
2000      outline: none;
2001      border-color: var(--color-phosphor);
2002    }
2003  
2004    .input::placeholder {
2005      color: var(--color-text-muted);
2006    }
2007  
2008    .btn-secondary {
2009      padding: 0.5rem 1rem;
2010      background-color: transparent;
2011      color: var(--color-phosphor);
2012      border: 1px solid var(--color-phosphor);
2013      border-radius: 4px;
2014      font-weight: 500;
2015      cursor: pointer;
2016      transition: background-color 0.2s;
2017      white-space: nowrap;
2018    }
2019  
2020    .btn-secondary:hover:not(:disabled) {
2021      background-color: rgba(217, 137, 46, 0.1);
2022    }
2023  
2024    .btn-secondary:disabled {
2025      opacity: 0.5;
2026      cursor: not-allowed;
2027    }
2028  
2029    .btn-danger {
2030      padding: 0.5rem 1rem;
2031      background-color: transparent;
2032      color: var(--color-status-error);
2033      border: 1px solid var(--color-status-error);
2034      border-radius: 4px;
2035      font-weight: 500;
2036      cursor: pointer;
2037      transition: background-color 0.2s;
2038    }
2039  
2040    .btn-danger:hover {
2041      background-color: rgba(218, 54, 51, 0.1);
2042    }
2043  
2044    .api-key-actions {
2045      display: flex;
2046      gap: 0.75rem;
2047    }
2048  
2049    .setting-description a {
2050      color: var(--color-phosphor);
2051      text-decoration: underline;
2052    }
2053  
2054    .setting-description a:hover {
2055      color: var(--color-phosphor-light);
2056    }
2057  
2058    /* Filesystem sync styles */
2059    .filesystem-config {
2060      margin-top: 1rem;
2061      padding: 1rem;
2062      background: rgba(0, 0, 0, 0.2);
2063      border-radius: 6px;
2064    }
2065  
2066    .filesystem-status {
2067      display: flex;
2068      align-items: center;
2069      gap: 0.5rem;
2070      margin-bottom: 0.75rem;
2071    }
2072  
2073    .filesystem-folder {
2074      margin-bottom: 0.75rem;
2075      padding: 0.5rem 0.75rem;
2076      background: rgba(0, 0, 0, 0.2);
2077      border-radius: 4px;
2078      font-size: 0.875rem;
2079    }
2080  
2081    .folder-label {
2082      color: var(--color-text-secondary);
2083      margin-right: 0.5rem;
2084    }
2085  
2086    .folder-path {
2087      color: var(--color-phosphor);
2088      font-family: var(--font-mono);
2089    }
2090  
2091    .filesystem-actions {
2092      margin-bottom: 0.75rem;
2093    }
2094  
2095    .filesystem-error {
2096      color: var(--color-status-error);
2097      font-size: 0.875rem;
2098      margin: 0.5rem 0;
2099    }
2100  
2101    .filesystem-instructions {
2102      margin-top: 0.5rem;
2103    }
2104  
2105    .filesystem-instructions .note {
2106      margin-top: 0.5rem;
2107      padding: 0.5rem 0.75rem;
2108      background: rgba(217, 137, 46, 0.1);
2109      border-radius: 4px;
2110      color: var(--color-phosphor);
2111    }
2112  
2113    /* Native messaging sync styles */
2114    .native-config {
2115      margin-top: 1rem;
2116      padding: 1rem;
2117      background: rgba(0, 0, 0, 0.2);
2118      border-radius: 6px;
2119    }
2120  
2121    .native-status {
2122      display: flex;
2123      align-items: center;
2124      gap: 0.5rem;
2125      margin-bottom: 0.75rem;
2126    }
2127  
2128    .native-info {
2129      margin-bottom: 0.75rem;
2130      padding: 0.75rem;
2131      background: rgba(0, 0, 0, 0.2);
2132      border-radius: 4px;
2133    }
2134  
2135    .info-row {
2136      display: flex;
2137      align-items: center;
2138      gap: 0.5rem;
2139      margin-bottom: 0.375rem;
2140    }
2141  
2142    .info-row:last-child {
2143      margin-bottom: 0;
2144    }
2145  
2146    .info-label {
2147      color: var(--color-text-secondary);
2148      font-size: 0.875rem;
2149      min-width: 110px;
2150    }
2151  
2152    .info-value {
2153      color: var(--color-text-primary);
2154      font-size: 0.875rem;
2155    }
2156  
2157    code.info-value {
2158      color: var(--color-phosphor);
2159      font-family: var(--font-mono);
2160      font-size: 0.8125rem;
2161      background: rgba(217, 137, 46, 0.1);
2162      padding: 0.125rem 0.375rem;
2163      border-radius: 3px;
2164    }
2165  
2166    .native-actions {
2167      display: flex;
2168      gap: 0.5rem;
2169      margin-bottom: 0.75rem;
2170    }
2171  
2172    .native-error {
2173      display: flex;
2174      align-items: center;
2175      gap: 0.5rem;
2176      color: var(--color-status-error);
2177      font-size: 0.875rem;
2178      margin-bottom: 0.75rem;
2179      padding: 0.5rem 0.75rem;
2180      background: rgba(239, 68, 68, 0.1);
2181      border-radius: 4px;
2182    }
2183  
2184    .native-error .error-icon {
2185      width: 1rem;
2186      height: 1rem;
2187      flex-shrink: 0;
2188    }
2189  
2190    .native-instructions {
2191      margin-top: 0.75rem;
2192      padding: 1rem;
2193      background: rgba(217, 137, 46, 0.05);
2194      border-radius: 6px;
2195      border: 1px solid rgba(217, 137, 46, 0.15);
2196    }
2197  
2198    .native-instructions h4 {
2199      margin: 0 0 0.75rem 0;
2200      font-size: 0.875rem;
2201      color: var(--color-phosphor);
2202    }
2203  
2204    .native-instructions ol {
2205      margin: 0;
2206      padding-left: 1.25rem;
2207      font-size: 0.875rem;
2208      color: var(--color-text-secondary);
2209    }
2210  
2211    .native-instructions li {
2212      margin-bottom: 0.5rem;
2213      line-height: 1.5;
2214    }
2215  
2216    .native-instructions code {
2217      background: rgba(217, 137, 46, 0.1);
2218      padding: 0.125rem 0.375rem;
2219      border-radius: 3px;
2220      font-family: var(--font-mono);
2221      font-size: 0.8125rem;
2222      color: var(--color-phosphor);
2223    }
2224  
2225    .setup-note {
2226      margin-top: 0.75rem;
2227      padding: 0.75rem;
2228      background: rgba(217, 137, 46, 0.1);
2229      border-radius: 4px;
2230      font-size: 0.8125rem;
2231      line-height: 1.5;
2232      color: var(--color-text-secondary);
2233    }
2234  
2235    .status-testing {
2236      background-color: rgba(59, 130, 246, 0.15);
2237      color: #60a5fa;
2238    }
2239  
2240    .logseq-instructions {
2241      margin-top: 1rem;
2242      padding: 1rem;
2243      background: rgba(217, 137, 46, 0.05);
2244      border-radius: 6px;
2245      border: 1px solid rgba(217, 137, 46, 0.15);
2246    }
2247  
2248    .logseq-instructions h4 {
2249      margin: 0 0 0.75rem 0;
2250      font-size: 0.875rem;
2251      color: var(--color-phosphor);
2252    }
2253  
2254    .logseq-instructions ol {
2255      margin: 0;
2256      padding-left: 1.25rem;
2257    }
2258  
2259    .logseq-instructions li {
2260      font-size: 0.8125rem;
2261      color: var(--color-text-muted);
2262      margin-bottom: 0.5rem;
2263    }
2264  
2265    .logseq-instructions li:last-child {
2266      margin-bottom: 0;
2267    }
2268  
2269    /* Todoist Integration */
2270    .todoist-info {
2271      margin-bottom: 0.75rem;
2272      padding: 0.75rem;
2273      background: rgba(0, 0, 0, 0.2);
2274      border-radius: 4px;
2275    }
2276  
2277    .todoist-instructions {
2278      margin-top: 1rem;
2279      padding: 1rem;
2280      background: rgba(217, 137, 46, 0.05);
2281      border-radius: 6px;
2282      border: 1px solid rgba(217, 137, 46, 0.15);
2283    }
2284  
2285    .todoist-instructions h4 {
2286      margin: 0 0 0.75rem 0;
2287      font-size: 0.875rem;
2288      color: var(--color-phosphor);
2289    }
2290  
2291    .todoist-instructions ol {
2292      margin: 0;
2293      padding-left: 1.25rem;
2294    }
2295  
2296    .todoist-instructions li {
2297      font-size: 0.8125rem;
2298      color: var(--color-text-muted);
2299      margin-bottom: 0.5rem;
2300    }
2301  
2302    .todoist-instructions li:last-child {
2303      margin-bottom: 0;
2304    }
2305  
2306    /* Pause Media Sites */
2307    .pause-media-section {
2308      grid-column: 1 / -1;
2309    }
2310  
2311    .pause-media-status {
2312      display: flex;
2313      align-items: center;
2314      gap: 0.5rem;
2315    }
2316  
2317    .site-count {
2318      padding: 0.25rem 0.5rem;
2319      background: rgba(46, 160, 67, 0.15);
2320      color: #2ea043;
2321      border-radius: 4px;
2322      font-size: 0.75rem;
2323      font-weight: 500;
2324    }
2325  
2326    .site-count.custom {
2327      background: rgba(217, 137, 46, 0.15);
2328      color: var(--color-phosphor);
2329    }
2330  
2331    .divider {
2332      color: var(--color-text-muted);
2333      margin: 0 0.25rem;
2334    }
2335  
2336    .pause-media-preview {
2337      padding: 1rem;
2338      background: rgba(0, 0, 0, 0.2);
2339      border-radius: 6px;
2340      border: 1px solid rgba(217, 137, 46, 0.15);
2341    }
2342  
2343    .sites-list {
2344      font-size: 0.875rem;
2345      margin-bottom: 0.75rem;
2346    }
2347  
2348    .sites-list:last-child {
2349      margin-bottom: 0;
2350    }
2351  
2352    .sites-list strong {
2353      color: var(--color-text-primary);
2354      display: block;
2355      margin-bottom: 0.25rem;
2356    }
2357  
2358    .sites-preview {
2359      color: var(--color-text-muted);
2360      word-break: break-word;
2361    }
2362  
2363    .custom-list {
2364      margin-top: 0.75rem;
2365      padding-top: 0.75rem;
2366      border-top: 1px solid rgba(217, 137, 46, 0.15);
2367    }
2368  
2369    .pause-media-actions {
2370      display: flex;
2371      gap: 0.75rem;
2372      align-items: center;
2373      flex-wrap: wrap;
2374    }
2375  
2376    .sites-textarea {
2377      width: 100%;
2378      padding: 0.75rem;
2379      background-color: var(--color-bg-dark);
2380      color: var(--color-text-primary);
2381      border: 1px solid rgba(217, 137, 46, 0.3);
2382      border-radius: 6px;
2383      font-family: var(--font-mono);
2384      font-size: 0.8125rem;
2385      line-height: 1.5;
2386      resize: vertical;
2387      min-height: 300px;
2388    }
2389  
2390    .sites-textarea:focus {
2391      outline: none;
2392      border-color: var(--color-phosphor);
2393    }
2394  
2395    .pause-media-editor .setting-description {
2396      margin: 0.5rem 0 1rem 0;
2397    }
2398  
2399    .pause-media-editor .setting-description code {
2400      background: rgba(217, 137, 46, 0.1);
2401      padding: 0.125rem 0.375rem;
2402      border-radius: 3px;
2403      font-family: var(--font-mono);
2404      font-size: 0.8125rem;
2405      color: var(--color-phosphor);
2406    }
2407  
2408    .pause-media-info {
2409      margin-top: 1rem;
2410    }
2411  
2412    .pause-media-info .note {
2413      margin: 0;
2414      padding: 0.75rem;
2415      background: rgba(217, 137, 46, 0.1);
2416      border-radius: 4px;
2417      font-size: 0.8125rem;
2418      line-height: 1.5;
2419    }
2420  
2421    .pause-media-info .note code {
2422      background: rgba(0, 0, 0, 0.2);
2423      padding: 0.125rem 0.375rem;
2424      border-radius: 3px;
2425      font-family: var(--font-mono);
2426    }
2427  
2428    .btn-danger-outline {
2429      padding: 0.5rem 1rem;
2430      background: transparent;
2431      color: #ef4444;
2432      border: 1px solid rgba(239, 68, 68, 0.4);
2433      border-radius: 4px;
2434      font-weight: 500;
2435      cursor: pointer;
2436      transition: all 0.2s;
2437    }
2438  
2439    .btn-danger-outline:hover {
2440      background: rgba(239, 68, 68, 0.1);
2441      border-color: rgba(239, 68, 68, 0.6);
2442    }
2443  
2444    .save-indicator {
2445      color: #2ea043;
2446      font-size: 0.875rem;
2447      font-weight: 500;
2448    }
2449  
2450    /* Graph Access for Attachments */
2451    .graph-access-section {
2452      margin-top: 1.25rem;
2453      padding-top: 1.25rem;
2454      border-top: 1px solid rgba(217, 137, 46, 0.15);
2455    }
2456  
2457    .subsection-title {
2458      font-size: 0.9375rem;
2459      color: var(--color-text-primary);
2460      margin: 0 0 0.5rem 0;
2461      font-weight: 500;
2462    }
2463  
2464    .graph-access-status {
2465      margin: 0.75rem 0;
2466      padding: 0.75rem;
2467      background: rgba(0, 0, 0, 0.15);
2468      border-radius: 6px;
2469    }
2470  
2471    .graph-directory-info {
2472      margin-top: 0.5rem;
2473      font-size: 0.875rem;
2474    }
2475  
2476    .directory-label {
2477      color: var(--color-text-secondary);
2478      margin-right: 0.5rem;
2479    }
2480  
2481    .directory-path {
2482      color: var(--color-phosphor);
2483      font-family: var(--font-mono);
2484    }
2485  
2486    .setting-description code {
2487      background: rgba(0, 0, 0, 0.2);
2488      padding: 0.125rem 0.375rem;
2489      border-radius: 3px;
2490      font-family: var(--font-mono);
2491      font-size: 0.8125rem;
2492    }
2493  
2494    .warning-text {
2495      color: var(--color-status-warning);
2496    }
2497  
2498    .btn-secondary.btn-danger {
2499      color: var(--color-status-error);
2500      border-color: var(--color-status-error);
2501    }
2502  
2503    .btn-secondary.btn-danger:hover:not(:disabled) {
2504      background-color: rgba(218, 54, 51, 0.1);
2505    }
2506  
2507    /* Security / Credential Vault Styles */
2508    .vault-setup-prompt {
2509      display: flex;
2510      align-items: center;
2511      gap: 1rem;
2512      padding: 1rem;
2513      background: rgba(217, 137, 46, 0.05);
2514      border: 1px solid rgba(217, 137, 46, 0.2);
2515      border-radius: 8px;
2516    }
2517  
2518    .vault-prompt-text {
2519      flex: 1;
2520    }
2521  
2522    .vault-prompt-text strong {
2523      color: var(--color-text-primary);
2524    }
2525  
2526    .vault-prompt-text p {
2527      margin: 0.25rem 0 0;
2528      font-size: 0.875rem;
2529      color: var(--color-text-muted);
2530    }
2531  
2532    .shield-icon {
2533      width: 40px;
2534      height: 40px;
2535      color: var(--color-phosphor);
2536      flex-shrink: 0;
2537    }
2538  
2539    .shield-icon svg {
2540      width: 100%;
2541      height: 100%;
2542    }
2543  
2544    .vault-setup-form {
2545      padding: 1rem;
2546      background: rgba(0, 0, 0, 0.15);
2547      border-radius: 8px;
2548    }
2549  
2550    .vault-setup-form h4 {
2551      margin: 0 0 1rem;
2552      color: var(--color-text-primary);
2553    }
2554  
2555    .vault-setup-form .input-group {
2556      margin-bottom: 1rem;
2557    }
2558  
2559    .vault-setup-form .input-group label {
2560      display: block;
2561      margin-bottom: 0.375rem;
2562      font-size: 0.875rem;
2563      color: var(--color-text-secondary);
2564    }
2565  
2566    .vault-setup-form .button-row {
2567      display: flex;
2568      justify-content: flex-end;
2569      gap: 0.75rem;
2570      margin-top: 1rem;
2571    }
2572  
2573    .vault-status {
2574      padding: 1rem;
2575      background: rgba(46, 160, 67, 0.05);
2576      border: 1px solid rgba(46, 160, 67, 0.2);
2577      border-radius: 8px;
2578    }
2579  
2580    .vault-status-header {
2581      display: flex;
2582      align-items: center;
2583      gap: 1rem;
2584      margin-bottom: 1rem;
2585    }
2586  
2587    .vault-icon {
2588      width: 36px;
2589      height: 36px;
2590      flex-shrink: 0;
2591    }
2592  
2593    .vault-icon svg {
2594      width: 100%;
2595      height: 100%;
2596    }
2597  
2598    .vault-icon.unlocked {
2599      color: #2ea043;
2600    }
2601  
2602    .vault-icon.locked {
2603      color: var(--color-phosphor);
2604    }
2605  
2606    .vault-status-text strong {
2607      color: var(--color-text-primary);
2608    }
2609  
2610    .vault-status-text p {
2611      margin: 0.25rem 0 0;
2612      font-size: 0.875rem;
2613      color: var(--color-text-muted);
2614    }
2615  
2616    .vault-credentials-list {
2617      display: flex;
2618      gap: 0.75rem;
2619      flex-wrap: wrap;
2620      margin-bottom: 1rem;
2621      padding: 0.75rem;
2622      background: rgba(0, 0, 0, 0.15);
2623      border-radius: 6px;
2624    }
2625  
2626    .cred-item {
2627      font-size: 0.8125rem;
2628      padding: 0.25rem 0.5rem;
2629      border-radius: 4px;
2630    }
2631  
2632    .cred-item.stored {
2633      background: rgba(46, 160, 67, 0.15);
2634      color: #2ea043;
2635    }
2636  
2637    .cred-item.empty {
2638      background: rgba(100, 100, 100, 0.15);
2639      color: var(--color-text-muted);
2640    }
2641  
2642    .vault-actions {
2643      display: flex;
2644      gap: 0.75rem;
2645    }
2646  
2647    .vault-locked {
2648      padding: 1rem;
2649      background: rgba(217, 137, 46, 0.05);
2650      border: 1px solid rgba(217, 137, 46, 0.2);
2651      border-radius: 8px;
2652    }
2653  
2654    .vault-locked .vault-icon {
2655      margin-bottom: 0.75rem;
2656    }
2657  
2658    .vault-locked-text {
2659      margin-bottom: 1rem;
2660    }
2661  
2662    .vault-locked-text strong {
2663      color: var(--color-text-primary);
2664    }
2665  
2666    .vault-locked-text p {
2667      margin: 0.25rem 0 0;
2668      font-size: 0.875rem;
2669      color: var(--color-text-muted);
2670    }
2671  
2672    .vault-locked .input-group {
2673      display: flex;
2674      gap: 0.75rem;
2675    }
2676  
2677    .vault-locked .input-group .text-input {
2678      flex: 1;
2679    }
2680  
2681    .warning-box {
2682      display: flex;
2683      align-items: flex-start;
2684      gap: 0.75rem;
2685      padding: 0.75rem;
2686      background: rgba(245, 158, 11, 0.1);
2687      border: 1px solid rgba(245, 158, 11, 0.3);
2688      border-radius: 6px;
2689      font-size: 0.8125rem;
2690      color: var(--color-text-secondary);
2691    }
2692  
2693    .warning-box svg {
2694      width: 1.25rem;
2695      height: 1.25rem;
2696      color: #f59e0b;
2697      flex-shrink: 0;
2698      margin-top: 0.125rem;
2699    }
2700  
2701    .success-message {
2702      display: flex;
2703      align-items: center;
2704      gap: 0.5rem;
2705      padding: 0.75rem;
2706      background: rgba(46, 160, 67, 0.1);
2707      border-radius: 6px;
2708      color: #2ea043;
2709      font-size: 0.875rem;
2710      margin-bottom: 1rem;
2711    }
2712  
2713    .success-message svg {
2714      width: 1rem;
2715      height: 1rem;
2716      flex-shrink: 0;
2717    }
2718  
2719    .error-message {
2720      display: flex;
2721      align-items: center;
2722      gap: 0.5rem;
2723      padding: 0.75rem;
2724      background: rgba(239, 68, 68, 0.1);
2725      border-radius: 6px;
2726      color: #ef4444;
2727      font-size: 0.875rem;
2728      margin-bottom: 1rem;
2729    }
2730  
2731    .error-message svg {
2732      width: 1rem;
2733      height: 1rem;
2734      flex-shrink: 0;
2735    }
2736  
2737    .security-info {
2738      margin-top: 1rem;
2739      padding: 1rem;
2740      background: rgba(217, 137, 46, 0.05);
2741      border-radius: 6px;
2742      border: 1px solid rgba(217, 137, 46, 0.15);
2743    }
2744  
2745    .security-info h4 {
2746      margin: 0 0 0.75rem 0;
2747      font-size: 0.875rem;
2748      color: var(--color-phosphor);
2749    }
2750  
2751    .security-info ul {
2752      margin: 0;
2753      padding-left: 1.25rem;
2754    }
2755  
2756    .security-info li {
2757      font-size: 0.8125rem;
2758      color: var(--color-text-muted);
2759      margin-bottom: 0.375rem;
2760    }
2761  
2762    .security-info li:last-child {
2763      margin-bottom: 0;
2764    }
2765  
2766    .button-row {
2767      display: flex;
2768      justify-content: flex-end;
2769      gap: 0.75rem;
2770      margin-top: 1rem;
2771    }
2772  
2773    /* Responsive: stack on smaller screens */
2774    @media (max-width: 768px) {
2775      .options {
2776        padding: 1.5rem;
2777      }
2778  
2779      .options-content {
2780        grid-template-columns: 1fr;
2781      }
2782  
2783      .options-content .settings-section:nth-last-child(-n+2) {
2784        grid-column: auto;
2785      }
2786  
2787      .options-footer {
2788        justify-content: stretch;
2789      }
2790  
2791      .btn-primary {
2792        width: 100%;
2793      }
2794    }
2795  </style>