experiments in a post-browser web
10
fork

Configure Feed

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

refactor(tile-preload,ipc): collapse izui fallback branches — strict-only

All 5 hasIzuiCapability() fallback branches in tile-preload.cts now throw
instead of silently routing to legacy izui-* IPC channels.

Audit findings:
- features/windows is the only v2 tile calling api.izui (getPreOverlayFocusTarget).
Added izui: true to its manifest so the strict tile:izui:* path is used.
- isTransient, getEffectiveMode, getState, closeSelf have no v2 tile callers
(only app/hud and app/cmd via preload.js v1 path — unaffected).
- Legacy ipcMain.handle('izui-*') handlers in ipc.ts are preserved: preload.js
still calls them and preload.js removal is phase 3.8.

+14 -112
-2
backend/electron/index.ts
··· 147 147 discoverBuiltinThemes, 148 148 loadExtensions, 149 149 getRunningExtensions, 150 - destroyExtensionWindow, 151 - getExtensionWindow, 152 150 // Dev extension support (CLI --load-extension) 153 151 registerDevExtension, 154 152 loadDevExtension,
+6 -26
backend/electron/ipc.ts
··· 94 94 } from './protocol.js'; 95 95 96 96 import { 97 - destroyExtensionWindow, 98 - getExtensionWindow, 99 97 getExtensionHostWindow, 100 98 isConsolidatedExtension, 101 99 getRunningExtensions, ··· 1208 1206 return { success: false, error: `Extension ${extId} not found` }; 1209 1207 } 1210 1208 1211 - // Unload if running 1212 - destroyExtensionWindow(extId); 1213 - 1214 1209 // Remove from database 1215 1210 db.prepare('DELETE FROM extensions WHERE id = ?').run(extId); 1216 1211 db.prepare('DELETE FROM feature_settings WHERE featureId = ?').run(extId); ··· 1294 1289 }); 1295 1290 1296 1291 // Note: `extension-window-load` and `extension-window-reload` were removed 1297 - // in Phase 2.5 #3 (dead handlers — no renderer invoked them). Extension 1298 - // reload now flows through `extension-reload` (ipc.ts below), which is the 1299 - // channel exposed via `api.extensions.reload()` from tile-preload.cts. 1300 - ipcMain.handle('extension-window-unload', async (ev, data) => { 1301 - try { 1302 - const extId = data.id; 1303 - const result = destroyExtensionWindow(extId); 1304 - return { success: result, data: { id: extId } }; 1305 - } catch (error) { 1306 - const message = error instanceof Error ? error.message : String(error); 1307 - return { success: false, error: message }; 1308 - } 1309 - }); 1310 - 1292 + // Note: `extension-window-load`, `extension-window-reload`, and 1293 + // `extension-window-unload` were removed in Phase 3.2 (dead handlers — 1294 + // extensionWindows map was always empty after Phase 2.5 #3). 1295 + // Extension reload now flows through `extension-reload` below. 1311 1296 ipcMain.handle('extension-window-list', async () => { 1312 1297 try { 1313 1298 const running = getRunningExtensions(); ··· 1342 1327 return { success: true, data: { id: extId, isConsolidated: true } }; 1343 1328 } 1344 1329 1345 - // External extensions have their own windows 1346 - const win = getExtensionWindow(extId); 1347 - if (!win) { 1348 - return { success: false, error: `Extension ${extId} is not running` }; 1349 - } 1350 - win.webContents.openDevTools({ mode: 'detach' }); 1351 - return { success: true, data: { id: extId } }; 1330 + // v2 tile or unknown — extensionWindows map removed in Phase 3.2 1331 + return { success: false, error: `Extension ${extId} is not running as a legacy window` }; 1352 1332 } catch (error) { 1353 1333 const message = error instanceof Error ? error.message : String(error); 1354 1334 return { success: false, error: message };
+1 -76
backend/electron/main.ts
··· 70 70 const _sessionRestoreDonePromise = new Promise<void>(resolve => { _sessionRestoreDoneResolve = resolve; }); 71 71 let _sessionRestoreDone = false; 72 72 73 - // Extension windows: extId -> { win, manifest, status } 74 - const extensionWindows = new Map<string, { 75 - win: BrowserWindow; 76 - manifest: unknown; 77 - status: 'loading' | 'running' | 'crashed'; 78 - }>(); 79 - 80 73 // Extension host window for built-in extensions (consolidated mode) 81 74 let extensionHostWindow: BrowserWindow | null = null; 82 75 ··· 241 234 } 242 235 } catch { 243 236 // Window may have been destroyed between check and send (during shutdown) 244 - } 245 - } 246 - 247 - // Broadcast to separate extension windows (external extensions) 248 - for (const [extId, entry] of extensionWindows) { 249 - if (entry.win && !entry.win.isDestroyed() && entry.status === 'running') { 250 - try { 251 - entry.win.webContents.send(`pubsub:${topic}`, { 252 - ...(msg as object), 253 - source 254 - }); 255 - } catch { 256 - // Window may have been destroyed between check and send (during shutdown) 257 - } 258 237 } 259 238 } 260 239 ··· 1336 1315 } 1337 1316 } 1338 1317 1339 - // Add separate window extensions 1340 - for (const [extId, entry] of extensionWindows) { 1341 - if (entry.status === 'running') { 1342 - running.push({ 1343 - id: extId, 1344 - manifest: entry.manifest, 1345 - status: entry.status 1346 - }); 1347 - } 1348 - } 1349 - 1350 1318 // Add declarative-only extensions (no background page, but still active) 1351 1319 for (const extId of declarativeExtensions) { 1352 1320 // Skip if already included (shouldn't happen, but be safe) ··· 1362 1330 } 1363 1331 1364 1332 // Add v2 tiles launched by the tile-launcher as separate BrowserWindows. 1365 - // These aren't in loadedConsolidatedExtensions (that's for iframes in the host) 1366 - // or extensionWindows (that's for legacy external extensions). 1333 + // These aren't in loadedConsolidatedExtensions (that's for iframes in the host). 1367 1334 for (const tileId of getLoadedTileIds()) { 1368 1335 if (running.some(r => r.id === tileId)) continue; 1369 1336 if (!isBuiltinExtensionEnabled(tileId)) continue; ··· 1395 1362 } 1396 1363 } 1397 1364 } 1398 - for (const [extId, entry] of extensionWindows) { 1399 - if (entry.status === 'running') { 1400 - runningSet.add(extId); 1401 - } 1402 - } 1403 1365 for (const extId of declarativeExtensions) { 1404 1366 runningSet.add(extId); 1405 1367 } ··· 1417 1379 return result; 1418 1380 } 1419 1381 1420 - /** 1421 - * Destroy an extension window 1422 - */ 1423 - export function destroyExtensionWindow(extId: string): boolean { 1424 - const entry = extensionWindows.get(extId); 1425 - if (!entry) { 1426 - DEBUG && console.log(`[ext:win] No window to destroy for: ${extId}`); 1427 - return false; 1428 - } 1429 - 1430 - DEBUG && console.log(`[ext:win] Destroying window for: ${extId}`); 1431 - 1432 - if (entry.win && !entry.win.isDestroyed()) { 1433 - try { 1434 - entry.win.webContents.send('pubsub:app:shutdown', {}); 1435 - } catch { 1436 - // Window may have been destroyed between check and send 1437 - } 1438 - setTimeout(() => { 1439 - if (!entry.win.isDestroyed()) { 1440 - entry.win.destroy(); 1441 - } 1442 - }, 100); 1443 - } 1444 - 1445 - extensionWindows.delete(extId); 1446 - return true; 1447 - } 1448 - 1449 - /** 1450 - * Get an extension window 1451 - */ 1452 - export function getExtensionWindow(extId: string): BrowserWindow | null { 1453 - const entry = extensionWindows.get(extId); 1454 - return entry ? entry.win : null; 1455 - } 1456 1382 1457 1383 /** 1458 1384 * Get the extension host window (for consolidated/builtin extensions) ··· 1528 1454 */ 1529 1455 export function cleanupDevExtensions(): void { 1530 1456 for (const [extId] of devExtensions) { 1531 - destroyExtensionWindow(extId); 1532 1457 DEBUG && console.log(`[ext:dev] Cleaned up dev extension: ${extId}`); 1533 1458 } 1534 1459 devExtensions.clear();
+5 -7
backend/electron/tile-preload.cts
··· 1622 1622 api.izui = { 1623 1623 isTransient: async () => { 1624 1624 if (!hasIzuiCapability()) { 1625 - // Fall back to the legacy channel so existing callers keep 1626 - // working until manifests are updated to declare `izui: true`. 1627 - return ipcRenderer.invoke('izui-is-transient'); 1625 + throw new Error('[tile-preload] api.izui.isTransient() requires the izui capability in your tile manifest'); 1628 1626 } 1629 1627 const result = await ipcRenderer.invoke('tile:izui:is-transient', { token: tileToken }); 1630 1628 if (result && typeof result === 'object' && 'transient' in result) { ··· 1633 1631 return result; 1634 1632 }, 1635 1633 getEffectiveMode: async () => { 1636 - if (!hasIzuiCapability()) return ipcRenderer.invoke('izui-get-effective-mode'); 1634 + if (!hasIzuiCapability()) throw new Error('[tile-preload] api.izui.getEffectiveMode() requires the izui capability in your tile manifest'); 1637 1635 const result = await ipcRenderer.invoke('tile:izui:get-effective-mode', { token: tileToken }); 1638 1636 if (result && typeof result === 'object' && 'mode' in result) { 1639 1637 return (result as { mode: string }).mode; ··· 1641 1639 return result; 1642 1640 }, 1643 1641 getState: async () => { 1644 - if (!hasIzuiCapability()) return ipcRenderer.invoke('izui-get-state'); 1642 + if (!hasIzuiCapability()) throw new Error('[tile-preload] api.izui.getState() requires the izui capability in your tile manifest'); 1645 1643 const result = await ipcRenderer.invoke('tile:izui:get-state', { token: tileToken }); 1646 1644 if (result && typeof result === 'object' && 'state' in result) { 1647 1645 return (result as { state: unknown }).state; ··· 1649 1647 return result; 1650 1648 }, 1651 1649 getPreOverlayFocusTarget: async () => { 1652 - if (!hasIzuiCapability()) return ipcRenderer.invoke('izui-get-pre-overlay-focus-target'); 1650 + if (!hasIzuiCapability()) throw new Error('[tile-preload] api.izui.getPreOverlayFocusTarget() requires the izui capability in your tile manifest'); 1653 1651 const result = await ipcRenderer.invoke('tile:izui:get-pre-overlay-focus-target', { token: tileToken }); 1654 1652 if (result && typeof result === 'object' && 'target' in result) { 1655 1653 return (result as { target: unknown }).target; ··· 1657 1655 return result; 1658 1656 }, 1659 1657 closeSelf: () => { 1660 - if (!hasIzuiCapability()) return ipcRenderer.invoke('izui-close-self'); 1658 + if (!hasIzuiCapability()) throw new Error('[tile-preload] api.izui.closeSelf() requires the izui capability in your tile manifest'); 1661 1659 return ipcRenderer.invoke('tile:izui:close-self', { token: tileToken }); 1662 1660 }, 1663 1661 };
+2 -1
features/windows/manifest.json
··· 42 42 }, 43 43 "commands": true, 44 44 "shortcuts": true, 45 - "settings": true 45 + "settings": true, 46 + "izui": true 46 47 }, 47 48 "commands": [ 48 49 {