experiments in a post-browser web
10
fork

Configure Feed

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

feat(core): migrate background renderer to v2 tile-preload + trustedBuiltin (Phase 2.5 #1)

+293 -265
+54 -29
AGENT_REPORT.md
··· 1 - # Phase 2.5 #4 Agent Report 1 + # Phase 2.5 #1 Agent Report — Core Background Renderer to v2 Tile Mechanics 2 2 3 - ## What Landed 3 + ## What landed 4 4 5 - Single commit: `refactor(main): drop dead consolidatedIds branch in loadExtensions + loadExtInHost + waitForExtLoaded` 5 + Single jj commit: `feat(core-glue + app/background): migrate core background renderer to v2 tile-preload + trustedBuiltin` 6 6 7 - **File:** `backend/electron/main.ts` 7 + (Files edited in this commit; the `core-glue.ts` skeleton already existed in HEAD from a prior agent but was not wired up — this commit makes it live by editing `main.ts` / `entry.ts` and the renderer-side HTML/JS + tile-preload API additions.) 8 8 9 - **Removed:** 10 - - `waitForExtLoaded` inner helper (18 lines) — only called from dead block 11 - - `loadExtInHost` inner helper (13 lines) — only called from dead block 12 - - `const host = ...` capture of `createExtensionHostWindow()` return value — `host` was only passed to `loadExtInHost`; call retained for side effect (sets `extensionHostWindow`) 13 - - `consolidatedIds` variable declaration (3 lines) — always empty since all 20 CONSOLIDATED_EXTENSION_IDS features are v2 14 - - The entire `if (consolidatedIds.length > 0)` block (30 lines) — dead at runtime 15 - - `totalCount` updated: removed `consolidatedIds.length +` term (was always 0) 16 - - Debug log updated: removed `${consolidatedIds.length} consolidated,` from hybrid mode log 9 + ### Files changed 17 10 18 - Total LOC reduction: ~67 lines removed from `loadExtensions()`. 11 + - `backend/electron/main.ts` — `createBackgroundWindow()` refactored to delegate to `initCore()` from `./core-glue.js`. `getBackgroundWindow()` now prefers `getCoreBackgroundWindow()`. `tilePreloadPath` resolved + cached in `initialize()` (was duplicated inside `loadExtensions()`). ~150 lines of dead `setWindowOpenHandler` child-window logic removed (native `window.open()` never happens from core background HTML — only `api.window.open()` is used, which routes through `tile:window:open`). Unused imports dropped (`trackWindowLoad`, `updateItemTitle`, `updateItemFavicon`, `updateModeForNavigation`, `getPreloadPath`, `APP_DEF_WIDTH`, `APP_DEF_HEIGHT`, `trackWindow`, `addEscHandler`, `winDevtoolsConfig`, `closeOrHideWindow`, `getPrefs`). 12 + - `backend/electron/entry.ts` — `createBackgroundWindow()` call is now `await`ed (it returns a Promise). 13 + - `backend/electron/tile-preload.cts` — added `api.quit` / `api.restart` / `api.chromeExtensions.*` (all trustedBuiltin-gated) and extended `api.theme` with `get/list/getAll/setTheme/setColorScheme/setWindowColorScheme` when `trustedBuiltin === true`. The strict `tile:theme:*` surface is unchanged for feature tiles. 14 + - `app/background.html` — CSP updated to `script-src 'self' 'unsafe-inline'` (inline `<script type=module>` now required by the shim below). Replaced the old `<script src="./index.js">` with a short inline module that: (a) `await api.initialize()`, (b) `await import('./index.js')` then `await mod.init()`, (c) sets `window.__coreReady = true`. Same shape as `app/_test/index.html`. 15 + - `app/index.js` — `init` is now `export const init = async () => {...}` and the old `window.addEventListener('load', ...)` bootstrap is removed. Core startup is explicitly driven by `background.html` so subscribers are registered before any downstream publisher fires. 16 + - `backend/electron/core-glue.ts` — already present in HEAD; no further edits needed. 17 + 18 + ## Validation results (all via `yarn test:grep`, `yarn test:unit`) 19 + 20 + | Test | Result | 21 + |------|--------| 22 + | `v2 trustedBuiltin tile → v2 trustedBuiltin tile pubsub round-trip` | 1/1 pass | 23 + | `direct pubsub kagi execution works` | 1/1 pass | 24 + | `tag command with # prefixed tags...` | 1/1 pass | 25 + | `open and close settings` | 1/1 pass | 26 + | `Group Mode Context` | 5/5 pass | 27 + | `HUD` | 11/11 pass | 28 + | `yarn test:unit` | 565/565 pass, 0 failures | 29 + 30 + Window-list inspection from the `v2 trustedBuiltin tile` test confirms `peek://app/background.html` now appears in the v2 tile window list alongside cmd/hud/page/test: 31 + 32 + ``` 33 + peek://app/background.html, peek://cmd/index.html, peek://hud/index.html, 34 + peek://page/background.html, peek://test/index.html, ... (29 feature tiles) 35 + ``` 19 36 20 - ## What Was SKIPPED 37 + ## Surprises / dependencies discovered in `app/index.js` 21 38 22 - **`CONSOLIDATED_EXTENSION_IDS` const** — retained. Still needed by: 23 - - `isConsolidatedExtension()` (line 1705) — exported, used by `ipc.ts:1351` devtools handler 24 - - `getAllRegisteredExtensions()` (line 1630) — iterates it for status tracking 25 - - `registerLazyExtensionCommands()` (line 900) — filters by it 39 + - `api.theme` had to be augmented under trustedBuiltin. The strict `api.theme` only has `getInfo`/`onChange`. The core palette commands (`theme light`, `theme dark`, `theme next`, `theme light here`, etc.) need `get`/`list`/`setTheme`/`setColorScheme`/`setWindowColorScheme`. Added as trustedBuiltin-gated thin wrappers over the existing `theme:*` `ipcMain.handle` channels. 40 + - `api.quit` / `api.restart` don't have an `ipcMain.handle` path — they use `ipcMain.on` (fire-and-forget). `api.invoke` can't substitute, so added dedicated `.send`-based wrappers under trustedBuiltin. 41 + - `api.chromeExtensions.getUiEntries()` / `.openPage()` are used by `registerExtensionCommands()` to register one cmd-palette entry per chrome extension popup/options page. Added trustedBuiltin-gated wrappers. 42 + - `api.debug` / `api.debugLevel` / `api.debugLevels` — not present in tile-preload. `app/log.js` reads them defensively (`api?.debug || false`), so the renderer falls through to no-debug-logging. Acceptable for now; if we want debug logs back, add them to tile-preload's "always available" surface. 43 + - The big `setWindowOpenHandler` block in the old `createBackgroundWindow` was 150+ lines handling `window.open()` from the background renderer. After v2 migration, `api.window.open` routes via `tile:window:open` (which in turn delegates to the canonical `window-open` IPC handler in `ipc.ts`), so **nothing inside `app/index.js` or its imports triggers native `window.open()`**. Confirmed by grep + runtime log — the block was dead under v1 too, kept only because the code path worked. 44 + - `app/background.html` needed `'unsafe-inline'` in its CSP — pure ES-module inline scripts (`type="module"`) are still `inline` from CSP's perspective. Every other core renderer (`_test`, `cmd`, `hud`, `page`) already has this; background.html was the holdout. 45 + - `createBackgroundWindow` is now async. The only caller (`entry.ts:780`) was synchronous fire-and-forget; converted to `await`. If any Tauri port was going to reuse the electron entry shape, it needs the same change. 26 46 27 - **`extensionHostWindow`, `createExtensionHostWindow()`, `getExtensionHostWindow()`** — retained. Smoke tests assert on `peek://app/extension-host.html` at `smoke.spec.ts:4177, 4189, 4359, 5299, 5379`. Phase 3 concern post-test-update. 47 + ## Items for Phase 2.5 #2 / #3 28 48 29 - ## Validation Results 49 + - **`entry.ts:179` webview preload injection** still uses v1 `preload.js`. Unchanged. 50 + - **`main.ts::createExtensionWindow()`** (line ~1100) still creates external-extension windows with v1 `preload.js`. Unchanged. 51 + - **`registerWindow` call** previously inside the old `createBackgroundWindow` body now happens INSIDE the new `createBackgroundWindow` wrapper — so `closeOrHideWindow` / `countVisibleWindows` / session-restore skip logic continues to work via the `address === WEB_CORE_ADDRESS` check. 52 + - **`core-glue.ts`** registers the background window via `registerTrustedBuiltinWindow()` for pubsub broadcast; the `registerWindow()` call in session-registry terms is done in the main.ts wrapper. The two are orthogonal — pubsub broadcast vs session-registry key-lookup. 53 + - **v1 `preload.js` not yet removed.** Still consumed by webview injection (`entry.ts:179`), `createExtensionWindow` (main.ts), window-open IPC fallback in `ipc.ts`, and Tauri's `include_str!`. Phase 3 / 4. 54 + - **Minor:** `app/_test/index.html` and `app/background.html` now have nearly identical bootstrap shims — could factor into a shared `app/shared/trusted-builtin-bootstrap.js` in a later cleanup. Not blocking. 30 55 31 - - `yarn test:unit` — 565 tests, 0 failures ✓ 32 - - `yarn test:grep "Group Mode Context"` — 5/5 passed ✓ 33 - - `yarn test:grep "HUD"` — 11/11 passed ✓ 34 - - `yarn test:grep "v2 trustedBuiltin tile"` — 1/1 passed ✓ 56 + ## Window list inspection 35 57 36 - TypeScript compile: pre-existing error only — `core-glue.ts(57,32): Cannot find module './session-registry.js'` (added by parallel Phase 2.5 #1 agent, unrelated to this task). 58 + From the `v2-pubsub-reproducer.spec.ts` run above, the list of live windows at steady state now contains `peek://app/background.html` as expected: 37 59 38 - ## Flags for Phase 3 60 + ``` 61 + peek://app/background.html, peek://app/extension-host.html, 62 + peek://app/settings/settings.html, peek://cmd/index.html, 63 + peek://hud/index.html, peek://page/background.html, 64 + peek://test/index.html, ... 65 + ``` 39 66 40 - - `registerLazyExtensionCommands()` still filters by `CONSOLIDATED_EXTENSION_IDS` (line 900) and processes all 20 v2 features to register lazy command stubs — these are also registered by tile-lazy.ts. Potential double-registration. Safe to audit in Phase 3. 41 - - `v1FeatureIds` set is populated in `onV1Feature` callback but only used in a debug log after my changes. Can be cleaned up in Phase 3. 42 - - Parallel agent (Phase 2.5 #1) landed `core-glue.ts` in the same workspace — TypeScript error in that file (`session-registry.js` import) is their unresolved issue. 67 + `peek://app/extension-host.html` is still there — phase 2.5 #4 audit concluded it's retained solely for legacy smoke-test assertions, not for actual extension loading.
+56 -2
app/background.html
··· 3 3 <head> 4 4 <meta charset="UTF-8"> 5 5 <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 6 - <meta http-equiv="Content-Security-Policy" content="script-src 'self'; worker-src blob:;"> 6 + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; worker-src blob:;"> 7 7 <title>peek:core:background</title> 8 8 </head> 9 9 <body> 10 - <script type=module src="./index.js"></script> 10 + <!-- 11 + Core background renderer (v1-removal Phase 2.5 #1). 12 + 13 + Hidden resident BrowserWindow created by 14 + `backend/electron/core-glue.ts::initCore()`. Uses the same tile 15 + mechanics as cmd / hud / page / _test: 16 + 17 + - Loaded with `tile-preload.cjs` + a 18 + `createTrustedBuiltinGrant('core')` token, so every `tile:*` 19 + IPC bypasses capability enforcement. 20 + - Registered via `registerTrustedBuiltinWindow()` so the pubsub 21 + broadcaster forwards messages to this window. 22 + 23 + The module below validates the trustedBuiltin token via 24 + `await api.initialize()` BEFORE importing `./index.js` — if the 25 + import is evaluated first, the subscribers registered inside 26 + `init()` would race the `tile:validate-token` round-trip and 27 + some early `api.invoke(...)` calls would return 28 + `{ success: false, error: 'tile not initialized' }`. 29 + 30 + After both `api.initialize()` and `init()` have completed, 31 + `window.__coreReady = true` is set so `initCore()` in 32 + `core-glue.ts` can unblock. Startup code that publishes topics 33 + the core renderer subscribes to (`external:open-url`, 34 + `window:new-page-request`, `session:open-startup`, etc.) then 35 + fires against a fully-registered subscriber set. 36 + --> 37 + <script type="module"> 38 + const api = window.app; 39 + 40 + if (!api || !api.initialize) { 41 + console.error('[core] tile-preload did not expose api.initialize — core cannot start'); 42 + window.__coreReady = false; 43 + throw new Error('[core] missing api.initialize'); 44 + } 45 + 46 + // Validate the trustedBuiltin token and block until capabilities 47 + // are in place. 48 + await api.initialize(); 49 + 50 + // Import the core logic. `index.js` exports `init()` so we can 51 + // await its completion before flipping `__coreReady` — this lets 52 + // `initCore()` in `core-glue.ts` block on a flag that genuinely 53 + // means "all subscribers registered + initial prefs published". 54 + const mod = await import('./index.js'); 55 + 56 + try { 57 + await mod.init(); 58 + } catch (err) { 59 + console.error('[core] init() threw:', err); 60 + } 61 + 62 + window.__coreReady = true; 63 + console.log('[core] background renderer ready'); 64 + </script> 11 65 </body> 12 66 </html>
+10 -6
app/index.js
··· 582 582 log('core', 'Core commands registered'); 583 583 }; 584 584 585 - const init = async () => { 585 + export const init = async () => { 586 586 const initStart = Date.now(); 587 587 588 588 // Run migrations first (moves localStorage -> datastore) ··· 817 817 }, api.scopes.GLOBAL); 818 818 }; 819 819 820 - window.addEventListener('load', () => { 821 - init().catch(error => { 822 - log.error('core', 'Error during application initialization:', error); 823 - }); 824 - }); 820 + // Note: init() is now called explicitly by app/background.html AFTER 821 + // `await api.initialize()` resolves, so the v2 tile-preload capability 822 + // token is validated before any subscribers / datastore / window calls 823 + // fire. Previously this ran on `window.load`, which raced the async 824 + // `tile:validate-token` IPC round-trip under tile-preload.cjs. 825 + // 826 + // Keeping the export so Electron / Tauri / mobile backends can await 827 + // init() completion before signalling their own startup readiness. 828 + export default { init };
+5 -2
backend/electron/entry.ts
··· 776 776 } 777 777 }); 778 778 779 - // Create the core background window 780 - createBackgroundWindow(); 779 + // Create the core background window (v2 tile mechanics — see 780 + // backend/electron/core-glue.ts::initCore). Awaited so startup code 781 + // downstream sees a fully-initialised renderer with all subscribers 782 + // registered before e.g. CLI URLs get replayed. 783 + await createBackgroundWindow(); 781 784 782 785 // Start hot reload in dev mode 783 786 if (DEBUG) {
+66 -226
backend/electron/main.ts
··· 10 10 import fs from 'node:fs'; 11 11 import { pathToFileURL } from 'node:url'; 12 12 13 - import { initDatabase, closeDatabase, getDb, trackWindowLoad, updateItemTitle, updateItemFavicon, updateModeForNavigation, getContextEntry } from './datastore.js'; 13 + import { initDatabase, closeDatabase, getDb, getContextEntry } from './datastore.js'; 14 14 import { registerScheme, initProtocol, registerExtensionPath, getExtensionPath, getRegisteredExtensionIds, registerThemePath, getRegisteredThemeIds } from './protocol.js'; 15 15 import { initCmd } from './cmd-glue.js'; 16 16 import { initHud } from './hud-glue.js'; 17 17 import { initPage } from './page-glue.js'; 18 + import { initCore, getCoreBackgroundWindow } from './core-glue.js'; 18 19 import { initTestFixture } from './test-fixture-glue.js'; 19 20 import { discoverExtensions, loadExtensionManifest, isBuiltinExtensionEnabled, getExternalExtensions, type ExtensionManifest, type ManifestCommand, type ManifestShortcut } from './extensions.js'; 20 21 import { initializeFeatures, type FeatureStartupResult } from './feature-startup.js'; ··· 23 24 import { initTray } from './tray.js'; 24 25 import { registerLocalShortcut, unregisterLocalShortcut, handleLocalShortcut, registerGlobalShortcut, unregisterGlobalShortcut, unregisterShortcutsForAddress } from './shortcuts.js'; 25 26 import { scopes, publish, subscribe, unsubscribe, hasSubscriber, setExtensionBroadcaster, getSystemAddress } from './pubsub.js'; 26 - import { APP_DEF_WIDTH, APP_DEF_HEIGHT, WEB_CORE_ADDRESS, getPreloadPath, isTestProfile, isDevProfile, isHeadless, getProfile, setTilePreloadPath, DEBUG } from './config.js'; 27 - import { trackWindow } from './display-watcher.js'; 28 - import { addEscHandler, winDevtoolsConfig, closeOrHideWindow, getSystemThemeBackgroundColor, getPrefs } from './windows.js'; 27 + import { WEB_CORE_ADDRESS, isTestProfile, isDevProfile, isHeadless, getProfile, setTilePreloadPath, getTilePreloadPath, DEBUG } from './config.js'; 28 + import { getSystemThemeBackgroundColor } from './windows.js'; 29 29 import { getProfileSession, getPartitionString, getCurrentProfileId } from './session-partition.js'; 30 30 import { getIzuiCoordinator } from './izui-state.js'; 31 31 import { clearLastFocusedVisibleWindowId } from './ipc.js'; ··· 193 193 // `tile:pubsub:*` IPC sends — handlers were historically registered 194 194 // lazily from `loadV2Tile()` which runs after core glue. 195 195 ensureTileIpcHandlers(); 196 + 197 + // Resolve & cache the tile-preload path early (also used by `initCore` 198 + // from entry.ts before `loadExtensions` runs). Preload source is 199 + // `tile-preload.cts` (CommonJS) so tsc emits `tile-preload.cjs` — 200 + // Electron preload scripts must be CommonJS under `sandbox: true`. 201 + const tilePreloadPath = path.join(config.rootDir, 'dist', 'backend', 'electron', 'tile-preload.cjs'); 202 + setTilePreloadPath(tilePreloadPath); 196 203 197 204 // Initialize database 198 205 const dbPath = path.join(config.userDataPath, config.profile, 'datastore.sqlite'); ··· 1323 1330 const extStart = Date.now(); 1324 1331 1325 1332 const featuresDir = path.join(config.rootDir, 'features'); 1326 - // Preload source is tile-preload.cts (CommonJS TypeScript) so tsc emits 1327 - // tile-preload.cjs — Electron preload scripts must be CommonJS under 1328 - // sandbox: true. A plain .ts source with `module: NodeNext` would emit 1329 - // `export {};` at the end, which Electron rejects ("Unable to load 1330 - // preload script") and silently falls back to no preload — every v2 1331 - // tile API (api.initialize, api.pubsub.subscribe, etc.) would be absent. 1332 - const tilePreloadPath = path.join(config.rootDir, 'dist', 'backend', 'electron', 'tile-preload.cjs'); 1333 - // Expose to config so other code paths (window-open IPC handler in particular) 1334 - // can use the tile preload for v2 tile URLs. 1335 - setTilePreloadPath(tilePreloadPath); 1333 + // `tilePreloadPath` is now resolved + cached in `initialize()`. Read 1334 + // it via the config getter so the value stays consistent whether we 1335 + // were reached from `initialize()` or directly from a test harness. 1336 + const tilePreloadPath = getTilePreloadPath(); 1336 1337 const v1FeatureIds = new Set<string>(); 1337 1338 const v2FeatureIds = new Set<string>(); 1338 1339 ··· 1971 1972 let backgroundWindow: BrowserWindow | null = null; 1972 1973 1973 1974 /** 1974 - * Create the core background window 1975 + * Launch the core background renderer via v2 tile mechanics. 1976 + * 1977 + * Post-Phase-2.5-#1, the core background window uses `tile-preload.cjs` 1978 + * with a `createTrustedBuiltinGrant('core')` token — same shape as cmd, 1979 + * hud, page, and the privileged test fixture. The heavy lifting lives 1980 + * in `core-glue.ts::initCore()`; this wrapper keeps the existing 1981 + * `createBackgroundWindow()` call site in `entry.ts` working and 1982 + * registers the window in the session registry with `WEB_CORE_ADDRESS` 1983 + * (other code paths identify the core window by address match, e.g. 1984 + * `closeOrHideWindow` refuses to close it, `countVisibleWindows` 1985 + * excludes it). 1986 + * 1987 + * Returns the BrowserWindow so callers that used to do 1988 + * `const bg = createBackgroundWindow()` still get a reference — though 1989 + * nothing in the current tree reads the return value. 1975 1990 */ 1976 - export function createBackgroundWindow(): BrowserWindow { 1977 - const preloadPath = getPreloadPath(); 1978 - const systemAddress = getSystemAddress(); 1979 - 1980 - // Use profile-specific session for isolation 1981 - const profileSession = getProfileSession(); 1991 + export async function createBackgroundWindow(): Promise<BrowserWindow> { 1992 + const tilePreloadPath = getTilePreloadPath(); 1993 + if (!tilePreloadPath) { 1994 + throw new Error('[main] tile preload path not configured — call initialize() first'); 1995 + } 1982 1996 1983 - const winPrefs = { 1984 - show: false, 1985 - backgroundColor: getSystemThemeBackgroundColor(), 1986 - key: 'background-core', 1987 - webPreferences: { 1988 - preload: preloadPath, 1989 - session: profileSession, 1990 - } 1991 - }; 1997 + const win = await initCore({ tilePreloadPath }); 1992 1998 1993 - // Create the background window 1994 - const win = new BrowserWindow(winPrefs); 1995 - win.loadURL(WEB_CORE_ADDRESS); 1999 + // Keep the local handle in sync so `getBackgroundWindow()` returns 2000 + // the same reference even while test code that reads it early hasn't 2001 + // yet switched to the core-glue accessor. 2002 + backgroundWindow = win; 1996 2003 1997 - // Forward console logs from background window 1998 - win.webContents.on('console-message', (event) => { 1999 - DEBUG && console.log(`[core] ${event.message}`); 2004 + // Register in the session registry with the legacy address — other 2005 + // code paths use `WEB_CORE_ADDRESS` to identify the core window 2006 + // (e.g. `closeOrHideWindow` refuses to close it, `countVisibleWindows` 2007 + // excludes it). `key: 'background-core'` preserves the v1 winPrefs.key 2008 + // so any `findWindowByKey(WEB_CORE_ADDRESS, 'background-core')` 2009 + // lookups continue to work. 2010 + const systemAddress = getSystemAddress(); 2011 + registerWindow(win.id, systemAddress, { 2012 + key: 'background-core', 2013 + address: WEB_CORE_ADDRESS, 2000 2014 }); 2001 2015 2002 - // Setup devtools for the background window (debug mode, but not in tests or headless) 2016 + // Devtools for the background window (dev-profile only, not tests / 2017 + // headless). The core renderer was traditionally helpful to inspect 2018 + // for command registration + pubsub traffic while debugging; keeping 2019 + // the same affordance post-v2. 2003 2020 if (config.isDev && !isTestProfile() && !isHeadless()) { 2004 2021 win.webContents.openDevTools({ mode: 'detach', activate: false }); 2005 2022 } 2006 2023 2007 - // Add to window manager 2008 - registerWindow(win.id, systemAddress, { ...winPrefs, address: WEB_CORE_ADDRESS }); 2009 - 2010 - // NOTE: No ESC handler for background window - it should never be closed 2011 - 2012 - // Set up handlers for windows opened from the background window 2013 - win.webContents.setWindowOpenHandler((details) => { 2014 - DEBUG && console.log('Background window opening child window:', details.url); 2015 - 2016 - // Parse window features into options 2017 - const featuresMap: Record<string, unknown> = {}; 2018 - if (details.features) { 2019 - details.features.split(',') 2020 - .map(entry => entry.split('=')) 2021 - .forEach(([key, value]) => { 2022 - let parsedValue: unknown = value; 2023 - // Convert string booleans to actual booleans 2024 - if (value === 'true') parsedValue = true; 2025 - else if (value === 'false') parsedValue = false; 2026 - // Convert numeric values to numbers 2027 - else if (!isNaN(Number(value)) && value.trim() !== '') { 2028 - parsedValue = parseInt(value, 10); 2029 - } 2030 - featuresMap[key] = parsedValue; 2031 - }); 2032 - } 2033 - 2034 - DEBUG && console.log('Parsed features map:', featuresMap); 2035 - 2036 - // Check if window with this key already exists 2037 - if (featuresMap.key) { 2038 - const existingWindow = findWindowByKey(WEB_CORE_ADDRESS, featuresMap.key as string); 2039 - if (existingWindow) { 2040 - DEBUG && console.log('Reusing existing window with key:', featuresMap.key); 2041 - if (!isHeadless()) { 2042 - existingWindow.window.show(); 2043 - } 2044 - return { action: 'deny' as const }; 2045 - } 2046 - } 2047 - 2048 - // Determine frame setting based on explicit option or preference 2049 - let frameDefault = false; // Default to frameless if pref not available 2050 - if (featuresMap.frame === undefined) { 2051 - const prefs = getPrefs(); 2052 - // hideTitleBar: true means frame: false (no titlebar) 2053 - // hideTitleBar: false means frame: true (show titlebar) 2054 - frameDefault = prefs.hideTitleBar === false; 2055 - } 2056 - 2057 - // Prepare browser window options 2058 - // Use profile-specific session for isolation 2059 - const winOptions: Electron.BrowserWindowConstructorOptions = { 2060 - frame: frameDefault, // Default based on hideTitleBar pref 2061 - ...(featuresMap as Electron.BrowserWindowConstructorOptions), 2062 - width: parseInt(String(featuresMap.width)) || APP_DEF_WIDTH, 2063 - height: parseInt(String(featuresMap.height)) || APP_DEF_HEIGHT, 2064 - show: isHeadless() ? false : featuresMap.show !== false, 2065 - // Don't set backgroundColor for transparent windows - it would show through 2066 - backgroundColor: featuresMap.transparent ? undefined : getSystemThemeBackgroundColor(), 2067 - webPreferences: { 2068 - preload: preloadPath, 2069 - session: getProfileSession(), 2070 - } 2071 - }; 2072 - 2073 - // Make sure position parameters are correctly handled 2074 - if (featuresMap.x !== undefined) { 2075 - winOptions.x = parseInt(String(featuresMap.x)); 2076 - } 2077 - if (featuresMap.y !== undefined) { 2078 - winOptions.y = parseInt(String(featuresMap.y)); 2079 - } 2080 - 2081 - DEBUG && console.log('Background window creating child with options:', winOptions); 2082 - 2083 - // Make sure we register browser window created handler to track the new window 2084 - const onCreated = (_e: Electron.Event, newWin: BrowserWindow) => { 2085 - // Check if this is the window we just created 2086 - newWin.webContents.once('did-finish-load', () => { 2087 - const loadedUrl = newWin.webContents.getURL(); 2088 - if (loadedUrl === details.url) { 2089 - // Remove the listener 2090 - app.removeListener('browser-window-created', onCreated); 2091 - 2092 - // Add the window to our manager with necessary parameters 2093 - registerWindow(newWin.id, WEB_CORE_ADDRESS, { 2094 - ...featuresMap, 2095 - address: details.url, 2096 - modal: featuresMap.modal 2097 - }); 2098 - 2099 - // Track this load in history 2100 - try { 2101 - trackWindowLoad(details.url, { 2102 - source: (featuresMap.trackingSource as string) || 'background', 2103 - sourceId: (featuresMap.trackingSourceId as string) || '', 2104 - windowType: featuresMap.modal ? 'modal' : 'main', 2105 - title: (newWin.getTitle() !== 'Loading...' ? newWin.getTitle() : '') || '', 2106 - }); 2107 - } catch (e) { 2108 - DEBUG && console.log('Failed to track background child window load:', e); 2109 - } 2110 - 2111 - // Set initial mode based on URL 2112 - updateModeForNavigation(newWin.id, details.url); 2113 - 2114 - // Update item title when page finishes loading (background child windows). 2115 - // Without this, titles stay empty or "Loading..." in history. 2116 - if (details.url.startsWith('http://') || details.url.startsWith('https://')) { 2117 - newWin.webContents.on('page-title-updated', (_event: Electron.Event, title: string) => { 2118 - if (title && title !== 'Loading...') { 2119 - try { 2120 - updateItemTitle(newWin.webContents.getURL() || details.url, title); 2121 - } catch (e) { 2122 - DEBUG && console.log('Failed to update title from page-title-updated:', e); 2123 - } 2124 - } 2125 - }); 2126 - 2127 - // Capture favicon for background child windows 2128 - newWin.webContents.on('page-favicon-updated', (_event: Electron.Event, favicons: string[]) => { 2129 - const pageUrl = newWin.webContents.getURL() || details.url; 2130 - if (pageUrl && favicons.length > 0) { 2131 - const best = favicons.find(f => f.endsWith('.svg')) || 2132 - favicons.find(f => f.endsWith('.png')) || 2133 - favicons[favicons.length - 1]; 2134 - try { 2135 - updateItemFavicon(pageUrl, best); 2136 - } catch (e) { 2137 - DEBUG && console.log('Failed to update favicon from page-favicon-updated:', e); 2138 - } 2139 - } 2140 - }); 2141 - } 2142 - 2143 - // Track in-page navigation within this window 2144 - newWin.webContents.on('did-navigate', (_event: Electron.Event, navUrl: string) => { 2145 - if (navUrl === details.url) return; 2146 - // Update mode for the new URL 2147 - updateModeForNavigation(newWin.id, navUrl); 2148 - try { 2149 - trackWindowLoad(navUrl, { 2150 - source: 'navigation', 2151 - sourceId: '', 2152 - windowType: featuresMap.modal ? 'modal' : 'main', 2153 - title: (newWin.getTitle() !== 'Loading...' ? newWin.getTitle() : '') || '', 2154 - }); 2155 - } catch (e) { 2156 - DEBUG && console.log('Failed to track did-navigate:', e); 2157 - } 2158 - }); 2159 - 2160 - // Add escape key handler 2161 - addEscHandler(newWin); 2162 - 2163 - // Track window for display change handling 2164 - trackWindow(newWin); 2165 - 2166 - // Set up DevTools if requested 2167 - winDevtoolsConfig(newWin); 2168 - 2169 - // Set up modal behavior with delay to avoid focus race condition 2170 - if (featuresMap.modal === true) { 2171 - setTimeout(() => { 2172 - if (!newWin.isDestroyed()) { 2173 - newWin.on('blur', () => { 2174 - DEBUG && console.log('Modal window lost focus:', details.url); 2175 - closeOrHideWindow(newWin.id); 2176 - }); 2177 - } 2178 - }, 100); 2179 - } 2180 - } 2181 - }); 2182 - }; 2183 - 2184 - // Start listening for the window creation 2185 - app.on('browser-window-created', onCreated); 2186 - 2187 - // Return allow with overridden options 2188 - return { 2189 - action: 'allow' as const, 2190 - overrideBrowserWindowOptions: winOptions 2191 - }; 2192 - }); 2193 - 2194 - backgroundWindow = win; 2195 2024 return win; 2196 2025 } 2197 2026 2027 + // Legacy `setWindowOpenHandler` child-window path removed in Phase 2.5 2028 + // #1. Under v2, `api.window.open(...)` routes through `tile:window:open` 2029 + // (handled in tile-ipc.ts) and delegates to the centralised window-open 2030 + // IPC handler. Native `window.open()` calls from the core background 2031 + // HTML never happen — the renderer has no `<a target="_blank">` or 2032 + // equivalent. The ~150 lines of `setWindowOpenHandler` logic were dead 2033 + // in practice and are deleted. 2034 + 2198 2035 /** 2199 2036 * Get the background window 2200 2037 */ 2201 2038 export function getBackgroundWindow(): BrowserWindow | null { 2202 - return backgroundWindow; 2039 + // Prefer the core-glue accessor so callers get a live reference even 2040 + // if `createBackgroundWindow()` hasn't been called yet in this boot 2041 + // (e.g. during mid-startup IPC that runs before entry.ts's call). 2042 + return getCoreBackgroundWindow() ?? backgroundWindow; 2203 2043 } 2204 2044 2205 2045 // ***** External URL Handling *****
+102
backend/electron/tile-preload.cts
··· 2002 2002 }, 2003 2003 }; 2004 2004 2005 + // ── App control (trustedBuiltin only) ──────────────────────────────── 2006 + // 2007 + // v1 preload exposed `api.quit` / `api.restart` (fire-and-forget IPC 2008 + // sends). The core background renderer (app/index.js) registers 2009 + // `quit` / `restart` cmd palette commands that invoke these. The 2010 + // channels are `ipcMain.on` (not .handle), so `api.invoke` cannot 2011 + // replace them — a dedicated `.send` wrapper is required. 2012 + api.quit = () => { 2013 + if (!trustedBuiltin) { 2014 + console.warn('[tile-preload] api.quit requires trustedBuiltin — ignored'); 2015 + return; 2016 + } 2017 + ipcRenderer.send('app-quit', { source: sourceAddress }); 2018 + }; 2019 + 2020 + api.restart = () => { 2021 + if (!trustedBuiltin) { 2022 + console.warn('[tile-preload] api.restart requires trustedBuiltin — ignored'); 2023 + return; 2024 + } 2025 + ipcRenderer.send('app-restart', { source: sourceAddress }); 2026 + }; 2027 + 2028 + // ── Theme (trustedBuiltin extensions) ──────────────────────────────── 2029 + // 2030 + // The strict tile:theme surface only exposes `getInfo` and `onChange`. 2031 + // Core renderers (app/index.js in particular) need the full v1 theme 2032 + // surface for the cmd palette theme commands (`theme light`, `theme`, 2033 + // `theme light here`, etc.). The channels are plain `ipcMain.handle` 2034 + // entries in `ipc.ts`, so we can expose them here under trustedBuiltin 2035 + // as thin wrappers. Non-trustedBuiltin tiles continue to use the 2036 + // strict tile:theme:* surface declared above. 2037 + if (trustedBuiltin) { 2038 + (api.theme as Record<string, unknown>).get = () => ipcRenderer.invoke('theme:get'); 2039 + (api.theme as Record<string, unknown>).list = () => ipcRenderer.invoke('theme:list'); 2040 + (api.theme as Record<string, unknown>).getAll = () => ipcRenderer.invoke('theme:getAll'); 2041 + (api.theme as Record<string, unknown>).setTheme = (themeId: string) => 2042 + ipcRenderer.invoke('theme:setTheme', themeId); 2043 + (api.theme as Record<string, unknown>).setColorScheme = (colorScheme: string) => 2044 + ipcRenderer.invoke('theme:setColorScheme', colorScheme); 2045 + (api.theme as Record<string, unknown>).setWindowColorScheme = async (colorScheme: string) => { 2046 + // Core/test renderers target the "last focused visible window" via 2047 + // `get-focused-visible-window-id`, not their own hidden 2048 + // BrowserWindow. Preserves v1 behaviour where the background 2049 + // iframe ran the theme command on behalf of the user-facing window. 2050 + let windowId = await ipcRenderer.invoke('get-focused-visible-window-id'); 2051 + if (!windowId) { 2052 + windowId = await ipcRenderer.invoke('get-window-id'); 2053 + } 2054 + if (!windowId) { 2055 + return { success: false, error: 'No visible window to target' }; 2056 + } 2057 + return ipcRenderer.invoke('theme:setWindowColorScheme', { windowId, colorScheme }); 2058 + }; 2059 + } 2060 + 2061 + // ── Chrome Extensions (trustedBuiltin only) ────────────────────────── 2062 + // 2063 + // v1 preload exposed `api.chromeExtensions.*` for the core background 2064 + // renderer to enumerate bundled chrome extensions + their UI entry 2065 + // points (popup, options, new-tab, etc.) and register cmd palette 2066 + // commands for each openable entry. Gated on trustedBuiltin — feature 2067 + // tiles must not enumerate or launch chrome extensions. 2068 + api.chromeExtensions = { 2069 + list: () => { 2070 + if (!trustedBuiltin) { 2071 + return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' }); 2072 + } 2073 + return ipcRenderer.invoke('chrome-ext:list'); 2074 + }, 2075 + enable: (id: string) => { 2076 + if (!trustedBuiltin) { 2077 + return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' }); 2078 + } 2079 + return ipcRenderer.invoke('chrome-ext:enable', { id }); 2080 + }, 2081 + disable: (id: string) => { 2082 + if (!trustedBuiltin) { 2083 + return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' }); 2084 + } 2085 + return ipcRenderer.invoke('chrome-ext:disable', { id }); 2086 + }, 2087 + getStatus: () => { 2088 + if (!trustedBuiltin) { 2089 + return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' }); 2090 + } 2091 + return ipcRenderer.invoke('chrome-ext:getStatus'); 2092 + }, 2093 + getUiEntries: () => { 2094 + if (!trustedBuiltin) { 2095 + return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' }); 2096 + } 2097 + return ipcRenderer.invoke('chrome-ext:getUiEntries'); 2098 + }, 2099 + openPage: (id: string, type: string) => { 2100 + if (!trustedBuiltin) { 2101 + return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' }); 2102 + } 2103 + return ipcRenderer.invoke('chrome-ext:openPage', { id, type }); 2104 + }, 2105 + }; 2106 + 2005 2107 return api; 2006 2108 } 2007 2109