experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(chrome-ext): generic settings/UI system for bundled Chrome extensions

Add infrastructure for opening bundled Chrome extension options pages
and popups from within Peek:

- Extend ChromeManifest type with options_page/options_ui fields
- Add ChromeExtensionUiEntry type for UI entry point metadata
- Add getChromeExtensionUiEntries() to discover options/popup pages
from loaded extensions' manifests
- Add openChromeExtensionPage() to open extension pages in dedicated
BrowserWindows with profile session (for chrome.* API access)
- Window reuse: re-focuses existing window if already open
- Add IPC handlers: chrome-ext:getUiEntries, chrome-ext:openPage
- Add preload API: api.chromeExtensions.getUiEntries(), openPage()
- Register command palette entries dynamically after chrome extensions
load (e.g. 'Consent-O-Matic Settings', 'Consent-O-Matic')
- Subscribe to cmd:execute topics from main process to handle execution
- Cleanup: close UI windows during extension manager shutdown

+231 -3
+137 -1
backend/electron/chrome-extensions.ts
··· 11 11 * - Compatible with electron-chrome-extensions for enhanced API support 12 12 */ 13 13 14 - import { session, Extension } from 'electron'; 14 + import { session, Extension, BrowserWindow } from 'electron'; 15 15 import fs from 'node:fs'; 16 16 import path from 'node:path'; 17 17 ··· 47 47 default_icon?: string | Record<string, string>; 48 48 default_title?: string; 49 49 }; 50 + options_page?: string; 51 + options_ui?: { 52 + page: string; 53 + open_in_tab?: boolean; 54 + }; 55 + } 56 + 57 + /** 58 + * UI entry point for a chrome extension (options page or popup) 59 + */ 60 + export interface ChromeExtensionUiEntry { 61 + extensionId: string; 62 + extensionName: string; 63 + type: 'options' | 'popup'; 64 + title: string; 65 + url: string; 66 + /** Suggested window dimensions */ 67 + width: number; 68 + height: number; 50 69 } 51 70 52 71 /** ··· 85 104 let extensionsDir: string | null = null; 86 105 const discoveredExtensions: Map<string, ChromeExtensionInfo> = new Map(); 87 106 const loadedExtensions: Map<string, LoadedChromeExtension> = new Map(); 107 + // Track open extension UI windows: key = `${extId}:${type}` -> BrowserWindow 108 + const extensionUiWindows: Map<string, BrowserWindow> = new Map(); 88 109 89 110 /** 90 111 * Initialize the chrome_extensions table in the database ··· 382 403 } 383 404 384 405 /** 406 + * Get UI entry points (options page, popup) for all loaded chrome extensions. 407 + * Returns entries only for extensions that are currently loaded and have UI pages. 408 + */ 409 + export function getChromeExtensionUiEntries(): ChromeExtensionUiEntry[] { 410 + const entries: ChromeExtensionUiEntry[] = []; 411 + 412 + for (const [extId, loaded] of loadedExtensions) { 413 + const discovered = discoveredExtensions.get(extId); 414 + if (!discovered) continue; 415 + 416 + const manifest = discovered.manifest; 417 + const baseUrl = loaded.electronExtension.url; // chrome-extension://[id]/ 418 + 419 + // Options page (options_ui takes priority over options_page) 420 + const optionsPage = manifest.options_ui?.page || manifest.options_page; 421 + if (optionsPage) { 422 + entries.push({ 423 + extensionId: extId, 424 + extensionName: discovered.name, 425 + type: 'options', 426 + title: `${discovered.name} Settings`, 427 + url: `${baseUrl}${optionsPage}`, 428 + width: 800, 429 + height: 600, 430 + }); 431 + } 432 + 433 + // Action popup 434 + if (manifest.action?.default_popup) { 435 + entries.push({ 436 + extensionId: extId, 437 + extensionName: discovered.name, 438 + type: 'popup', 439 + title: manifest.action.default_title || discovered.name, 440 + url: `${baseUrl}${manifest.action.default_popup}`, 441 + width: 400, 442 + height: 500, 443 + }); 444 + } 445 + } 446 + 447 + return entries; 448 + } 449 + 450 + /** 451 + * Open a chrome extension's UI page (options or popup) in a BrowserWindow. 452 + * Reuses an existing window if one is already open for this extension+type. 453 + * @returns The window ID on success, null on failure 454 + */ 455 + export function openChromeExtensionPage( 456 + extensionId: string, 457 + pageType: 'options' | 'popup' 458 + ): BrowserWindow | null { 459 + const windowKey = `${extensionId}:${pageType}`; 460 + 461 + // Reuse existing window if it's still open 462 + const existing = extensionUiWindows.get(windowKey); 463 + if (existing && !existing.isDestroyed()) { 464 + existing.focus(); 465 + return existing; 466 + } 467 + 468 + // Find the UI entry 469 + const entries = getChromeExtensionUiEntries(); 470 + const entry = entries.find(e => e.extensionId === extensionId && e.type === pageType); 471 + if (!entry) { 472 + console.error(`[chrome-ext] No ${pageType} page found for extension: ${extensionId}`); 473 + return null; 474 + } 475 + 476 + // Use profile-specific session so the extension page has access to chrome.* APIs 477 + const profileSession = getProfileSession(); 478 + 479 + const win = new BrowserWindow({ 480 + width: entry.width, 481 + height: entry.height, 482 + title: entry.title, 483 + show: true, 484 + webPreferences: { 485 + session: profileSession, 486 + // No preload — these are chrome extension pages, they use chrome.* APIs 487 + nodeIntegration: false, 488 + contextIsolation: true, 489 + }, 490 + }); 491 + 492 + // Track the window 493 + extensionUiWindows.set(windowKey, win); 494 + 495 + win.on('closed', () => { 496 + extensionUiWindows.delete(windowKey); 497 + DEBUG && console.log(`[chrome-ext] UI window closed: ${windowKey}`); 498 + }); 499 + 500 + win.loadURL(entry.url).catch(error => { 501 + console.error(`[chrome-ext] Failed to load ${pageType} page for ${extensionId}:`, error); 502 + if (!win.isDestroyed()) { 503 + win.destroy(); 504 + } 505 + }); 506 + 507 + DEBUG && console.log(`[chrome-ext] Opened ${pageType} page for ${extensionId}: ${entry.url}`); 508 + 509 + return win; 510 + } 511 + 512 + /** 385 513 * Clean up chrome extension manager 386 514 */ 387 515 export async function cleanupChromeExtensions(): Promise<void> { 388 516 DEBUG && console.log('[chrome-ext] Cleaning up...'); 517 + 518 + // Close any open UI windows 519 + for (const [key, win] of extensionUiWindows) { 520 + if (!win.isDestroyed()) { 521 + win.destroy(); 522 + } 523 + } 524 + extensionUiWindows.clear(); 389 525 390 526 for (const extId of loadedExtensions.keys()) { 391 527 await unloadExtension(extId);
+46 -2
backend/electron/entry.ts
··· 72 72 import { 73 73 initChromeExtensionManager, 74 74 loadEnabledChromeExtensions, 75 + getChromeExtensionUiEntries, 76 + openChromeExtensionPage, 75 77 cleanupChromeExtensions, 76 78 } from './chrome-extensions.js'; 77 79 import { ··· 273 275 DEBUG && console.log('[lifecycle] before-quit: cleanup complete'); 274 276 }); 275 277 278 + /** 279 + * Register command palette entries for chrome extension UI pages. 280 + * Called after chrome extensions are loaded so we know which ones 281 + * have options pages or popups. 282 + * 283 + * For each UI entry point, registers a command and subscribes to 284 + * its execution topic so the main process can open the window. 285 + */ 286 + function registerChromeExtensionCommands(): void { 287 + const entries = getChromeExtensionUiEntries(); 288 + if (entries.length === 0) return; 289 + 290 + const systemAddress = getSystemAddress(); 291 + 292 + for (const entry of entries) { 293 + const commandName = entry.type === 'options' 294 + ? `${entry.extensionName} Settings` 295 + : entry.extensionName; 296 + 297 + // Subscribe to execution topic so we can handle it from the main process 298 + const execTopic = `cmd:execute:${commandName}`; 299 + subscribe(systemAddress, scopes.GLOBAL, execTopic, () => { 300 + DEBUG && console.log(`[chrome-ext] Executing command: ${commandName}`); 301 + openChromeExtensionPage(entry.extensionId, entry.type); 302 + }); 303 + 304 + // Register the command with the cmd extension 305 + publish(systemAddress, scopes.GLOBAL, 'cmd:register', { 306 + name: commandName, 307 + description: entry.type === 'options' 308 + ? `Open ${entry.extensionName} settings` 309 + : `Open ${entry.extensionName} popup`, 310 + source: systemAddress, 311 + scope: 'global', 312 + }); 313 + } 314 + 315 + DEBUG && console.log(`[chrome-ext] Registered ${entries.length} command(s) for chrome extension UI pages`); 316 + } 317 + 276 318 // ***** init ***** 277 319 278 320 // Electron app load ··· 456 498 }); 457 499 } 458 500 459 - // Load enabled chrome extensions 460 - loadEnabledChromeExtensions().catch(err => { 501 + // Load enabled chrome extensions, then register their UI pages as commands 502 + loadEnabledChromeExtensions().then(() => { 503 + registerChromeExtensionCommands(); 504 + }).catch(err => { 461 505 console.error('[startup] Chrome extensions init failed:', err); 462 506 }); 463 507
+3
backend/electron/index.ts
··· 275 275 isChromeExtensionEnabled, 276 276 isChromeExtensionLoaded, 277 277 getChromeExtensionStatus, 278 + getChromeExtensionUiEntries, 279 + openChromeExtensionPage, 278 280 cleanupChromeExtensions, 279 281 } from './chrome-extensions.js'; 280 282 281 283 export type { 282 284 ChromeManifest, 283 285 ChromeExtensionInfo, 286 + ChromeExtensionUiEntry, 284 287 LoadedChromeExtension, 285 288 ChromeExtensionSetting, 286 289 } from './chrome-extensions.js';
+27
backend/electron/ipc.ts
··· 3650 3650 } 3651 3651 }); 3652 3652 3653 + // Get UI entry points for all loaded chrome extensions 3654 + ipcMain.handle('chrome-ext:getUiEntries', async () => { 3655 + try { 3656 + const { getChromeExtensionUiEntries } = await import('./chrome-extensions.js'); 3657 + const entries = getChromeExtensionUiEntries(); 3658 + return { success: true, data: entries }; 3659 + } catch (error) { 3660 + const message = error instanceof Error ? error.message : String(error); 3661 + return { success: false, error: message }; 3662 + } 3663 + }); 3664 + 3665 + // Open a chrome extension's options page or popup in a window 3666 + ipcMain.handle('chrome-ext:openPage', async (_ev, data: { id: string; type: 'options' | 'popup' }) => { 3667 + try { 3668 + const { openChromeExtensionPage } = await import('./chrome-extensions.js'); 3669 + const win = openChromeExtensionPage(data.id, data.type); 3670 + if (win) { 3671 + return { success: true, data: { windowId: win.id } }; 3672 + } 3673 + return { success: false, error: `No ${data.type} page available for extension: ${data.id}` }; 3674 + } catch (error) { 3675 + const message = error instanceof Error ? error.message : String(error); 3676 + return { success: false, error: message }; 3677 + } 3678 + }); 3679 + 3653 3680 DEBUG && console.log('[ipc] Web extension handlers registered'); 3654 3681 } 3655 3682
+18
preload.js
··· 911 911 */ 912 912 getStatus: () => { 913 913 return ipcRenderer.invoke('chrome-ext:getStatus'); 914 + }, 915 + 916 + /** 917 + * Get UI entry points for all loaded chrome extensions (options pages, popups) 918 + * @returns {Promise<{success: boolean, data?: Array<{extensionId: string, extensionName: string, type: string, title: string, url: string, width: number, height: number}>, error?: string}>} 919 + */ 920 + getUiEntries: () => { 921 + return ipcRenderer.invoke('chrome-ext:getUiEntries'); 922 + }, 923 + 924 + /** 925 + * Open a chrome extension's options page or popup in a window 926 + * @param {string} id - Extension ID 927 + * @param {'options' | 'popup'} type - Page type to open 928 + * @returns {Promise<{success: boolean, data?: {windowId: number}, error?: string}>} 929 + */ 930 + openPage: (id, type) => { 931 + return ipcRenderer.invoke('chrome-ext:openPage', { id, type }); 914 932 } 915 933 }; 916 934