experiments in a post-browser web
10
fork

Configure Feed

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

fix cmd ordering and registration after extraction

+58 -61
+57 -60
backend/electron/main.ts
··· 1325 1325 export async function loadExtensions(): Promise<number> { 1326 1326 const extStart = Date.now(); 1327 1327 1328 - // ── V2 Feature Startup ── 1329 - // Run the feature-startup pipeline: discover, sync registry, and load v2 tile manifests. 1330 - // V2 manifests are handled by the tile launcher; v1 manifests fall through to legacy loading below. 1331 1328 const featuresDir = path.join(config.rootDir, 'features'); 1332 1329 const tilePreloadPath = path.join(config.rootDir, 'backend', 'electron', 'tile-preload.js'); 1333 1330 const v1FeatureIds = new Set<string>(); 1334 1331 const v2FeatureIds = new Set<string>(); 1335 1332 1336 - try { 1337 - const featureResult: FeatureStartupResult = initializeFeatures({ 1338 - appDataDir: config.userDataPath, 1339 - builtinFeaturesDir: featuresDir, 1340 - tilePreloadPath, 1341 - eagerIds: EAGER_EXTENSION_IDS, 1342 - onV1Feature: (id: string, _featurePath: string, _manifest: unknown) => { 1343 - // Collect v1 feature IDs — they'll be loaded via the legacy path below 1344 - v1FeatureIds.add(id); 1345 - }, 1346 - }); 1347 - // Track which features the v2 tile system handled — skip these in legacy loading 1348 - for (const r of featureResult.loadResults) { 1349 - if (r.loaded) v2FeatureIds.add(r.id); 1350 - } 1351 - DEBUG && console.log( 1352 - `[ext] Feature startup: ${v2FeatureIds.size} v2 loaded, ` + 1353 - `${v1FeatureIds.size} v1 deferred to legacy loader` 1354 - ); 1355 - } catch (err) { 1356 - console.error('[ext] Feature startup failed, falling back to legacy-only loading:', err); 1357 - } 1358 - 1359 - // Get all registered extension IDs 1360 - const registeredExtIds = getRegisteredExtensionIds(); 1361 - 1362 - // Split into consolidated (built-in) and external — exclude v2 features already loaded by tile system 1363 - const consolidatedIds = registeredExtIds.filter(id => 1364 - CONSOLIDATED_EXTENSION_IDS.includes(id) && isBuiltinExtensionEnabled(id) && !v2FeatureIds.has(id) 1365 - ); 1366 - const externalBuiltinIds = registeredExtIds.filter(id => 1367 - !CONSOLIDATED_EXTENSION_IDS.includes(id) && isBuiltinExtensionEnabled(id) && !v2FeatureIds.has(id) 1368 - ); 1369 - 1370 - // Get external extensions from datastore 1371 - const externalExts = getExternalExtensions(); 1372 - const enabledExternalExts = externalExts.filter(ext => ext.enabled && ext.path); 1373 - 1374 - DEBUG && console.log(`[ext] Hybrid mode: ${consolidatedIds.length} consolidated, ${externalBuiltinIds.length + enabledExternalExts.length} external`); 1375 - 1376 - // Phase 1: Early 1377 - publish('system', scopes.GLOBAL, 'ext:startup:phase', { phase: 'early' }); 1378 - 1379 - // Helper: wait for an extension to appear in loadedConsolidatedExtensions 1380 - // (populated by did-frame-finish-load handler in createExtensionHostWindow) 1333 + // Helper: wait for an extension to appear in loadedConsolidatedExtensions. 1381 1334 const waitForExtLoaded = (extId: string, timeoutMs: number): Promise<void> => { 1382 1335 return new Promise<void>((resolve) => { 1383 1336 if (loadedConsolidatedExtensions.has(extId)) { resolve(); return; } ··· 1397 1350 }); 1398 1351 }; 1399 1352 1400 - // Helper: create iframe in extension host via executeJavaScript 1401 - // This bypasses IPC/contextBridge for reliability 1353 + // Helper: create iframe in extension host via executeJavaScript. 1402 1354 const loadExtInHost = (host: BrowserWindow, id: string, url: string): void => { 1403 1355 host.webContents.executeJavaScript(` 1404 1356 (function() { ··· 1414 1366 }; 1415 1367 1416 1368 // Subscribe to ext:ready — extensions publish this after registering their handlers. 1417 - // This resolves lazy-load promises so the stub can safely re-publish commands. 1418 1369 subscribe(getSystemAddress(), scopes.SYSTEM, 'ext:ready', (data: unknown) => { 1419 1370 const extId = (data as { id?: string })?.id; 1420 1371 const registeredTopics = (data as { registeredTopics?: string[] })?.registeredTopics; ··· 1423 1374 } 1424 1375 }); 1425 1376 1426 - // Always create the extension host window — cmd (the command registry) lives 1427 - // there as an iframe, even when no features are consolidated. This is core 1428 - // infrastructure, loaded via cmd-glue.ts before any feature registers commands. 1377 + // Load the extension host and cmd FIRST — cmd owns the command registry. 1378 + // Any feature that publishes cmd:register or cmd:register-batch must find 1379 + // cmd already subscribed, otherwise registration is lost. 1429 1380 const host = await createExtensionHostWindow(); 1381 + await initCmd({ host, loadExtInHost, waitForExtLoaded }); 1430 1382 1431 - // Phase 1a: Load cmd (command registry) via cmd-glue. This MUST complete 1432 - // before any feature tries to register commands via cmd:register pubsub. 1433 - await initCmd({ host, loadExtInHost, waitForExtLoaded }); 1383 + // ── V2 Feature Startup ── 1384 + // Run the feature-startup pipeline: discover, sync registry, and load v2 tile manifests. 1385 + // V2 manifests are handled by the tile launcher; v1 manifests fall through to legacy loading below. 1386 + // This runs AFTER initCmd so tile-lazy's cmd:register-batch publishes reach cmd. 1387 + try { 1388 + const featureResult: FeatureStartupResult = initializeFeatures({ 1389 + appDataDir: config.userDataPath, 1390 + builtinFeaturesDir: featuresDir, 1391 + tilePreloadPath, 1392 + eagerIds: EAGER_EXTENSION_IDS, 1393 + onV1Feature: (id: string, _featurePath: string, _manifest: unknown) => { 1394 + // Collect v1 feature IDs — they'll be loaded via the legacy path below 1395 + v1FeatureIds.add(id); 1396 + }, 1397 + }); 1398 + // Track which features the v2 tile system handled — skip these in legacy loading 1399 + for (const r of featureResult.loadResults) { 1400 + if (r.loaded) v2FeatureIds.add(r.id); 1401 + } 1402 + DEBUG && console.log( 1403 + `[ext] Feature startup: ${v2FeatureIds.size} v2 loaded, ` + 1404 + `${v1FeatureIds.size} v1 deferred to legacy loader` 1405 + ); 1406 + } catch (err) { 1407 + console.error('[ext] Feature startup failed, falling back to legacy-only loading:', err); 1408 + } 1409 + 1410 + // Get all registered extension IDs 1411 + const registeredExtIds = getRegisteredExtensionIds(); 1412 + 1413 + // Split into consolidated (built-in) and external — exclude v2 features already loaded by tile system 1414 + const consolidatedIds = registeredExtIds.filter(id => 1415 + CONSOLIDATED_EXTENSION_IDS.includes(id) && isBuiltinExtensionEnabled(id) && !v2FeatureIds.has(id) 1416 + ); 1417 + const externalBuiltinIds = registeredExtIds.filter(id => 1418 + !CONSOLIDATED_EXTENSION_IDS.includes(id) && isBuiltinExtensionEnabled(id) && !v2FeatureIds.has(id) 1419 + ); 1420 + 1421 + // Get external extensions from datastore 1422 + const externalExts = getExternalExtensions(); 1423 + const enabledExternalExts = externalExts.filter(ext => ext.enabled && ext.path); 1424 + 1425 + DEBUG && console.log(`[ext] Hybrid mode: ${consolidatedIds.length} consolidated, ${externalBuiltinIds.length + enabledExternalExts.length} external`); 1426 + 1427 + // Phase 1: Early 1428 + publish('system', scopes.GLOBAL, 'ext:startup:phase', { phase: 'early' }); 1434 1429 1435 1430 // Phase 2: Commands 1436 1431 publish('system', scopes.GLOBAL, 'ext:startup:phase', { phase: 'commands' }); ··· 1517 1512 export function getRunningExtensions(): Array<{ id: string; manifest: unknown; status: string }> { 1518 1513 const running: Array<{ id: string; manifest: unknown; status: string }> = []; 1519 1514 1520 - // Add consolidated extensions that have actually loaded (in extension host iframes) 1515 + // Add consolidated extensions that have actually loaded (in extension host iframes). 1516 + // Iterates loadedConsolidatedExtensions (populated by did-frame-finish-load), which 1517 + // includes both CONSOLIDATED_EXTENSION_IDS features AND core pseudo-extensions like 1518 + // cmd that are loaded by core glue (cmd-glue.ts) rather than by the feature system. 1521 1519 if (extensionHostWindow && !extensionHostWindow.isDestroyed()) { 1522 - for (const extId of CONSOLIDATED_EXTENSION_IDS) { 1520 + for (const extId of loadedConsolidatedExtensions) { 1523 1521 if (!isBuiltinExtensionEnabled(extId)) continue; 1524 - if (!loadedConsolidatedExtensions.has(extId)) continue; 1525 1522 const extPath = getExtensionPath(extId); 1526 1523 const manifest = extPath ? loadExtensionManifest(extPath) : null; 1527 1524 running.push({
+1 -1
tests/unit/item-query.test.js
··· 12 12 import { fileURLToPath } from 'url'; 13 13 14 14 const __dirname = dirname(fileURLToPath(import.meta.url)); 15 - const modulePath = join(__dirname, '..', '..', 'features', 'cmd', 'commands', 'extract-title.js'); 15 + const modulePath = join(__dirname, '..', '..', 'app', 'cmd', 'commands', 'extract-title.js'); 16 16 17 17 const { extractTitle } = await import(modulePath); 18 18