···1717import { initPage } from './page-glue.js';
1818import { initCore, getCoreBackgroundWindow } from './core-glue.js';
1919import { initTestFixture } from './test-fixture-glue.js';
2020-import { discoverExtensions, loadExtensionManifest, isBuiltinExtensionEnabled, getExternalExtensions, type ExtensionManifest, type ManifestCommand, type ManifestShortcut } from './extensions.js';
2020+import { discoverExtensions, loadExtensionManifest, isBuiltinExtensionEnabled, type ExtensionManifest, type ManifestCommand, type ManifestShortcut } from './extensions.js';
2121import { initializeFeatures, type FeatureStartupResult } from './feature-startup.js';
2222import { ensureTileIpcHandlers } from './tile-compat.js';
2323import { getLoadedTileIds, getTileManifest, getAllTileWindows, unloadAllTiles, relaunchTile } from './tile-launcher.js';
···11061106}
1107110711081108/**
11091109- * Create an extension window
11101110- *
11111111- * Legacy v1 path — creates a BrowserWindow for a registered extension with the
11121112- * v1 `preload.js`. Still used by `reloadExtension()` (features-manager UI reload
11131113- * button via `api.extensions.reload()`) and `loadDevExtension()` (CLI
11141114- * `--load-extension` flag). The two startup-path callers below
11151115- * (`externalBuiltinIds` + `enabledExternalExts` loops in `loadExtensions()`)
11161116- * are empty in practice — all features in `features/` are v2 and get loaded
11171117- * via `loadV2Tile()`; the `extensions` DB table is superseded by the v2
11181118- * feature registry. See Phase 2.5 #3 audit in AGENT_REPORT.md and
11191119- * `docs/v1-removal-plan.md`.
11201120- *
11211121- * Full removal requires a `relaunchTile(tileId)` helper in tile-launcher.ts —
11221122- * tracked as Phase 2.5 #3b.
11231123- */
11241124-async function createExtensionWindow(extId: string): Promise<BrowserWindow | null> {
11251125- if (extensionWindows.has(extId)) {
11261126- DEBUG && console.log(`[ext:win] Extension ${extId} already has a window`);
11271127- return extensionWindows.get(extId)!.win;
11281128- }
11291129-11301130- const extPath = getExtensionPath(extId);
11311131- if (!extPath) {
11321132- console.error(`[ext:win] Extension path not found: ${extId}`);
11331133- return null;
11341134- }
11351135-11361136- const manifest = loadExtensionManifest(extPath);
11371137-11381138- DEBUG && console.log(`[ext:win] Creating window for extension: ${extId}`);
11391139-11401140- // Use profile-specific session for isolation
11411141- const profileSession = getProfileSession();
11421142-11431143- const win = new BrowserWindow({
11441144- show: false,
11451145- backgroundColor: getSystemThemeBackgroundColor(),
11461146- webPreferences: {
11471147- preload: config.preloadPath,
11481148- session: profileSession,
11491149- // Disable same-origin policy so extensions can fetch() cross-origin.
11501150- // These windows only load trusted extension code (peek://ext/{id}/...) —
11511151- // never user-provided URLs. corsEnabled:false on the scheme isn't sufficient
11521152- // for peek://ext origins (only peek://app gets implicit cross-origin access).
11531153- webSecurity: false,
11541154- }
11551155- });
11561156-11571157- // Forward console logs
11581158- win.webContents.on('console-message', (event) => {
11591159- DEBUG && console.log(`[ext:${extId}] ${event.message}`);
11601160- });
11611161-11621162- // Track crashes
11631163- win.webContents.on('render-process-gone', (event, details) => {
11641164- console.error(`[ext:win] Extension ${extId} crashed (reason: ${details.reason})`);
11651165- const entry = extensionWindows.get(extId);
11661166- if (entry) {
11671167- entry.status = 'crashed';
11681168- }
11691169- });
11701170-11711171- // Track close
11721172- win.on('closed', () => {
11731173- DEBUG && console.log(`[ext:win] Extension ${extId} window closed`);
11741174- extensionWindows.delete(extId);
11751175- });
11761176-11771177- extensionWindows.set(extId, { win, manifest, status: 'loading' });
11781178-11791179- try {
11801180- await win.loadURL(`peek://ext/${extId}/background.html`);
11811181- DEBUG && console.log(`[ext:win] Extension ${extId} loaded successfully`);
11821182- const entry = extensionWindows.get(extId);
11831183- if (entry) {
11841184- entry.status = 'running';
11851185- }
11861186-11871187- // Open devtools for extension in debug mode (not in tests or headless)
11881188- if (config.isDev && !isTestProfile() && !isHeadless()) {
11891189- win.webContents.openDevTools({ mode: 'detach', activate: false });
11901190- }
11911191-11921192- return win;
11931193- } catch (error) {
11941194- console.error(`[ext:win] Failed to load extension ${extId}:`, error);
11951195- extensionWindows.delete(extId);
11961196- win.destroy();
11971197- return null;
11981198- }
11991199-}
12001200-12011201-/**
12021109 * Create the consolidated extension host window
12031110 * All extensions load as iframes within this single window
12041111 */
···13741281 console.error('[ext] Feature startup failed, falling back to legacy-only loading:', err);
13751282 }
1376128313771377- // Get all registered extension IDs
13781378- const registeredExtIds = getRegisteredExtensionIds();
13791379-13801380- // External built-in extensions: registered IDs not in the consolidated list and not loaded by
13811381- // the v2 tile system. All CONSOLIDATED_EXTENSION_IDS features are v2, so this only catches
13821382- // any remaining v1 external extensions (e.g. 'example') that aren't in v2FeatureIds.
13831383- const externalBuiltinIds = registeredExtIds.filter(id =>
13841384- !CONSOLIDATED_EXTENSION_IDS.includes(id) && isBuiltinExtensionEnabled(id) && !v2FeatureIds.has(id)
13851385- );
13861386-13871387- // Get external extensions from datastore
13881388- const externalExts = getExternalExtensions();
13891389- const enabledExternalExts = externalExts.filter(ext => ext.enabled && ext.path);
13901390-13911391- DEBUG && console.log(`[ext] Startup: ${externalBuiltinIds.length + enabledExternalExts.length} external`);
13921392-13931284 // Phase 1: Early
13941285 publish('system', scopes.GLOBAL, 'ext:startup:phase', { phase: 'early' });
13951286···14081299 // would try to load the v2 tile through the v1 iframe path.
14091300 registerLazyEventInterceptors({ skipExtensions: v2FeatureIds });
1410130114111411- // Load external built-in extensions (like 'example') as separate windows
14121412- // Skip declarative-only extensions — they don't need background pages
14131413- for (const extId of externalBuiltinIds) {
14141414- if (isDeclarativeOnly(extId)) continue;
14151415- try { await createExtensionWindow(extId); } catch (e) {
14161416- console.error(`[ext] Failed to load external built-in extension ${extId}:`, e);
14171417- }
14181418- }
14191419-14201420- // Load external extensions from datastore as separate windows
14211421- for (const ext of enabledExternalExts) {
14221422- if (!extensionWindows.has(ext.id)) {
14231423- registerExtensionPath(ext.id, ext.path!);
14241424- try { await createExtensionWindow(ext.id); } catch (e) {
14251425- console.error(`[ext] Failed to load external extension ${ext.id}:`, e);
14261426- }
14271427- }
14281428- }
14291429-14301430- const totalCount = externalBuiltinIds.length + enabledExternalExts.length;
14311302 DEBUG && console.log(`[ext:timing] hybrid total: ${Date.now() - extStart}ms`);
1432130314331304 // Phase 3: UI
···1435130614361307 // Phase 4: Complete
14371308 publish('system', scopes.GLOBAL, 'ext:startup:phase', { phase: 'complete' });
14381438- publish('system', scopes.GLOBAL, 'ext:all-loaded', { count: totalCount });
13091309+ publish('system', scopes.GLOBAL, 'ext:all-loaded', { count: 0 });
1439131014401440- return totalCount;
13111311+ return 0;
14411312}
1442131314431314/**
+1-3
backend/electron/tile-launcher.ts
···616616 * Relaunch a tile — re-read the manifest from disk, close the old window
617617 * (if any), revoke its tokens, then call `launchTile()` with the fresh manifest.
618618 *
619619- * This is the v2 replacement for `createExtensionWindow(extId)` used by
620620- * `reloadExtension()` and `loadDevExtension()` in main.ts. See Phase 2.5 #3b
621621- * in `docs/v1-removal-plan.md`.
619619+ * Used by `reloadExtension()` and `loadDevExtension()` in main.ts.
622620 *
623621 * Edge cases:
624622 * - Tile not currently running (no window): just launches fresh.