···11-# Phase 2.5 #4 Agent Report
11+# Phase 2.5 #1 Agent Report — Core Background Renderer to v2 Tile Mechanics
2233-## What Landed
33+## What landed
4455-Single commit: `refactor(main): drop dead consolidatedIds branch in loadExtensions + loadExtInHost + waitForExtLoaded`
55+Single jj commit: `feat(core-glue + app/background): migrate core background renderer to v2 tile-preload + trustedBuiltin`
6677-**File:** `backend/electron/main.ts`
77+(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.)
8899-**Removed:**
1010-- `waitForExtLoaded` inner helper (18 lines) — only called from dead block
1111-- `loadExtInHost` inner helper (13 lines) — only called from dead block
1212-- `const host = ...` capture of `createExtensionHostWindow()` return value — `host` was only passed to `loadExtInHost`; call retained for side effect (sets `extensionHostWindow`)
1313-- `consolidatedIds` variable declaration (3 lines) — always empty since all 20 CONSOLIDATED_EXTENSION_IDS features are v2
1414-- The entire `if (consolidatedIds.length > 0)` block (30 lines) — dead at runtime
1515-- `totalCount` updated: removed `consolidatedIds.length +` term (was always 0)
1616-- Debug log updated: removed `${consolidatedIds.length} consolidated,` from hybrid mode log
99+### Files changed
17101818-Total LOC reduction: ~67 lines removed from `loadExtensions()`.
1111+- `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`).
1212+- `backend/electron/entry.ts` — `createBackgroundWindow()` call is now `await`ed (it returns a Promise).
1313+- `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.
1414+- `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`.
1515+- `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.
1616+- `backend/electron/core-glue.ts` — already present in HEAD; no further edits needed.
1717+1818+## Validation results (all via `yarn test:grep`, `yarn test:unit`)
1919+2020+| Test | Result |
2121+|------|--------|
2222+| `v2 trustedBuiltin tile → v2 trustedBuiltin tile pubsub round-trip` | 1/1 pass |
2323+| `direct pubsub kagi execution works` | 1/1 pass |
2424+| `tag command with # prefixed tags...` | 1/1 pass |
2525+| `open and close settings` | 1/1 pass |
2626+| `Group Mode Context` | 5/5 pass |
2727+| `HUD` | 11/11 pass |
2828+| `yarn test:unit` | 565/565 pass, 0 failures |
2929+3030+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:
3131+3232+```
3333+peek://app/background.html, peek://cmd/index.html, peek://hud/index.html,
3434+peek://page/background.html, peek://test/index.html, ... (29 feature tiles)
3535+```
19362020-## What Was SKIPPED
3737+## Surprises / dependencies discovered in `app/index.js`
21382222-**`CONSOLIDATED_EXTENSION_IDS` const** — retained. Still needed by:
2323-- `isConsolidatedExtension()` (line 1705) — exported, used by `ipc.ts:1351` devtools handler
2424-- `getAllRegisteredExtensions()` (line 1630) — iterates it for status tracking
2525-- `registerLazyExtensionCommands()` (line 900) — filters by it
3939+- `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.
4040+- `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.
4141+- `api.chromeExtensions.getUiEntries()` / `.openPage()` are used by `registerExtensionCommands()` to register one cmd-palette entry per chrome extension popup/options page. Added trustedBuiltin-gated wrappers.
4242+- `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.
4343+- 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.
4444+- `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.
4545+- `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.
26462727-**`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.
4747+## Items for Phase 2.5 #2 / #3
28482929-## Validation Results
4949+- **`entry.ts:179` webview preload injection** still uses v1 `preload.js`. Unchanged.
5050+- **`main.ts::createExtensionWindow()`** (line ~1100) still creates external-extension windows with v1 `preload.js`. Unchanged.
5151+- **`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.
5252+- **`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.
5353+- **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.
5454+- **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.
30553131-- `yarn test:unit` — 565 tests, 0 failures ✓
3232-- `yarn test:grep "Group Mode Context"` — 5/5 passed ✓
3333-- `yarn test:grep "HUD"` — 11/11 passed ✓
3434-- `yarn test:grep "v2 trustedBuiltin tile"` — 1/1 passed ✓
5656+## Window list inspection
35573636-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).
5858+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:
37593838-## Flags for Phase 3
6060+```
6161+peek://app/background.html, peek://app/extension-host.html,
6262+peek://app/settings/settings.html, peek://cmd/index.html,
6363+peek://hud/index.html, peek://page/background.html,
6464+peek://test/index.html, ...
6565+```
39664040-- `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.
4141-- `v1FeatureIds` set is populated in `onV1Feature` callback but only used in a debug log after my changes. Can be cleaned up in Phase 3.
4242-- 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.
6767+`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
···33 <head>
44 <meta charset="UTF-8">
55 <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
66- <meta http-equiv="Content-Security-Policy" content="script-src 'self'; worker-src blob:;">
66+ <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; worker-src blob:;">
77 <title>peek:core:background</title>
88 </head>
99 <body>
1010- <script type=module src="./index.js"></script>
1010+ <!--
1111+ Core background renderer (v1-removal Phase 2.5 #1).
1212+1313+ Hidden resident BrowserWindow created by
1414+ `backend/electron/core-glue.ts::initCore()`. Uses the same tile
1515+ mechanics as cmd / hud / page / _test:
1616+1717+ - Loaded with `tile-preload.cjs` + a
1818+ `createTrustedBuiltinGrant('core')` token, so every `tile:*`
1919+ IPC bypasses capability enforcement.
2020+ - Registered via `registerTrustedBuiltinWindow()` so the pubsub
2121+ broadcaster forwards messages to this window.
2222+2323+ The module below validates the trustedBuiltin token via
2424+ `await api.initialize()` BEFORE importing `./index.js` — if the
2525+ import is evaluated first, the subscribers registered inside
2626+ `init()` would race the `tile:validate-token` round-trip and
2727+ some early `api.invoke(...)` calls would return
2828+ `{ success: false, error: 'tile not initialized' }`.
2929+3030+ After both `api.initialize()` and `init()` have completed,
3131+ `window.__coreReady = true` is set so `initCore()` in
3232+ `core-glue.ts` can unblock. Startup code that publishes topics
3333+ the core renderer subscribes to (`external:open-url`,
3434+ `window:new-page-request`, `session:open-startup`, etc.) then
3535+ fires against a fully-registered subscriber set.
3636+ -->
3737+ <script type="module">
3838+ const api = window.app;
3939+4040+ if (!api || !api.initialize) {
4141+ console.error('[core] tile-preload did not expose api.initialize — core cannot start');
4242+ window.__coreReady = false;
4343+ throw new Error('[core] missing api.initialize');
4444+ }
4545+4646+ // Validate the trustedBuiltin token and block until capabilities
4747+ // are in place.
4848+ await api.initialize();
4949+5050+ // Import the core logic. `index.js` exports `init()` so we can
5151+ // await its completion before flipping `__coreReady` — this lets
5252+ // `initCore()` in `core-glue.ts` block on a flag that genuinely
5353+ // means "all subscribers registered + initial prefs published".
5454+ const mod = await import('./index.js');
5555+5656+ try {
5757+ await mod.init();
5858+ } catch (err) {
5959+ console.error('[core] init() threw:', err);
6060+ }
6161+6262+ window.__coreReady = true;
6363+ console.log('[core] background renderer ready');
6464+ </script>
1165 </body>
1266</html>
+10-6
app/index.js
···582582 log('core', 'Core commands registered');
583583};
584584585585-const init = async () => {
585585+export const init = async () => {
586586 const initStart = Date.now();
587587588588 // Run migrations first (moves localStorage -> datastore)
···817817 }, api.scopes.GLOBAL);
818818};
819819820820-window.addEventListener('load', () => {
821821- init().catch(error => {
822822- log.error('core', 'Error during application initialization:', error);
823823- });
824824-});
820820+// Note: init() is now called explicitly by app/background.html AFTER
821821+// `await api.initialize()` resolves, so the v2 tile-preload capability
822822+// token is validated before any subscribers / datastore / window calls
823823+// fire. Previously this ran on `window.load`, which raced the async
824824+// `tile:validate-token` IPC round-trip under tile-preload.cjs.
825825+//
826826+// Keeping the export so Electron / Tauri / mobile backends can await
827827+// init() completion before signalling their own startup readiness.
828828+export default { init };
+5-2
backend/electron/entry.ts
···776776 }
777777 });
778778779779- // Create the core background window
780780- createBackgroundWindow();
779779+ // Create the core background window (v2 tile mechanics — see
780780+ // backend/electron/core-glue.ts::initCore). Awaited so startup code
781781+ // downstream sees a fully-initialised renderer with all subscribers
782782+ // registered before e.g. CLI URLs get replayed.
783783+ await createBackgroundWindow();
781784782785 // Start hot reload in dev mode
783786 if (DEBUG) {
+66-226
backend/electron/main.ts
···1010import fs from 'node:fs';
1111import { pathToFileURL } from 'node:url';
12121313-import { initDatabase, closeDatabase, getDb, trackWindowLoad, updateItemTitle, updateItemFavicon, updateModeForNavigation, getContextEntry } from './datastore.js';
1313+import { initDatabase, closeDatabase, getDb, getContextEntry } from './datastore.js';
1414import { registerScheme, initProtocol, registerExtensionPath, getExtensionPath, getRegisteredExtensionIds, registerThemePath, getRegisteredThemeIds } from './protocol.js';
1515import { initCmd } from './cmd-glue.js';
1616import { initHud } from './hud-glue.js';
1717import { initPage } from './page-glue.js';
1818+import { initCore, getCoreBackgroundWindow } from './core-glue.js';
1819import { initTestFixture } from './test-fixture-glue.js';
1920import { discoverExtensions, loadExtensionManifest, isBuiltinExtensionEnabled, getExternalExtensions, type ExtensionManifest, type ManifestCommand, type ManifestShortcut } from './extensions.js';
2021import { initializeFeatures, type FeatureStartupResult } from './feature-startup.js';
···2324import { initTray } from './tray.js';
2425import { registerLocalShortcut, unregisterLocalShortcut, handleLocalShortcut, registerGlobalShortcut, unregisterGlobalShortcut, unregisterShortcutsForAddress } from './shortcuts.js';
2526import { scopes, publish, subscribe, unsubscribe, hasSubscriber, setExtensionBroadcaster, getSystemAddress } from './pubsub.js';
2626-import { APP_DEF_WIDTH, APP_DEF_HEIGHT, WEB_CORE_ADDRESS, getPreloadPath, isTestProfile, isDevProfile, isHeadless, getProfile, setTilePreloadPath, DEBUG } from './config.js';
2727-import { trackWindow } from './display-watcher.js';
2828-import { addEscHandler, winDevtoolsConfig, closeOrHideWindow, getSystemThemeBackgroundColor, getPrefs } from './windows.js';
2727+import { WEB_CORE_ADDRESS, isTestProfile, isDevProfile, isHeadless, getProfile, setTilePreloadPath, getTilePreloadPath, DEBUG } from './config.js';
2828+import { getSystemThemeBackgroundColor } from './windows.js';
2929import { getProfileSession, getPartitionString, getCurrentProfileId } from './session-partition.js';
3030import { getIzuiCoordinator } from './izui-state.js';
3131import { clearLastFocusedVisibleWindowId } from './ipc.js';
···193193 // `tile:pubsub:*` IPC sends — handlers were historically registered
194194 // lazily from `loadV2Tile()` which runs after core glue.
195195 ensureTileIpcHandlers();
196196+197197+ // Resolve & cache the tile-preload path early (also used by `initCore`
198198+ // from entry.ts before `loadExtensions` runs). Preload source is
199199+ // `tile-preload.cts` (CommonJS) so tsc emits `tile-preload.cjs` —
200200+ // Electron preload scripts must be CommonJS under `sandbox: true`.
201201+ const tilePreloadPath = path.join(config.rootDir, 'dist', 'backend', 'electron', 'tile-preload.cjs');
202202+ setTilePreloadPath(tilePreloadPath);
196203197204 // Initialize database
198205 const dbPath = path.join(config.userDataPath, config.profile, 'datastore.sqlite');
···13231330 const extStart = Date.now();
1324133113251332 const featuresDir = path.join(config.rootDir, 'features');
13261326- // Preload source is tile-preload.cts (CommonJS TypeScript) so tsc emits
13271327- // tile-preload.cjs — Electron preload scripts must be CommonJS under
13281328- // sandbox: true. A plain .ts source with `module: NodeNext` would emit
13291329- // `export {};` at the end, which Electron rejects ("Unable to load
13301330- // preload script") and silently falls back to no preload — every v2
13311331- // tile API (api.initialize, api.pubsub.subscribe, etc.) would be absent.
13321332- const tilePreloadPath = path.join(config.rootDir, 'dist', 'backend', 'electron', 'tile-preload.cjs');
13331333- // Expose to config so other code paths (window-open IPC handler in particular)
13341334- // can use the tile preload for v2 tile URLs.
13351335- setTilePreloadPath(tilePreloadPath);
13331333+ // `tilePreloadPath` is now resolved + cached in `initialize()`. Read
13341334+ // it via the config getter so the value stays consistent whether we
13351335+ // were reached from `initialize()` or directly from a test harness.
13361336+ const tilePreloadPath = getTilePreloadPath();
13361337 const v1FeatureIds = new Set<string>();
13371338 const v2FeatureIds = new Set<string>();
13381339···19711972let backgroundWindow: BrowserWindow | null = null;
1972197319731974/**
19741974- * Create the core background window
19751975+ * Launch the core background renderer via v2 tile mechanics.
19761976+ *
19771977+ * Post-Phase-2.5-#1, the core background window uses `tile-preload.cjs`
19781978+ * with a `createTrustedBuiltinGrant('core')` token — same shape as cmd,
19791979+ * hud, page, and the privileged test fixture. The heavy lifting lives
19801980+ * in `core-glue.ts::initCore()`; this wrapper keeps the existing
19811981+ * `createBackgroundWindow()` call site in `entry.ts` working and
19821982+ * registers the window in the session registry with `WEB_CORE_ADDRESS`
19831983+ * (other code paths identify the core window by address match, e.g.
19841984+ * `closeOrHideWindow` refuses to close it, `countVisibleWindows`
19851985+ * excludes it).
19861986+ *
19871987+ * Returns the BrowserWindow so callers that used to do
19881988+ * `const bg = createBackgroundWindow()` still get a reference — though
19891989+ * nothing in the current tree reads the return value.
19751990 */
19761976-export function createBackgroundWindow(): BrowserWindow {
19771977- const preloadPath = getPreloadPath();
19781978- const systemAddress = getSystemAddress();
19791979-19801980- // Use profile-specific session for isolation
19811981- const profileSession = getProfileSession();
19911991+export async function createBackgroundWindow(): Promise<BrowserWindow> {
19921992+ const tilePreloadPath = getTilePreloadPath();
19931993+ if (!tilePreloadPath) {
19941994+ throw new Error('[main] tile preload path not configured — call initialize() first');
19951995+ }
1982199619831983- const winPrefs = {
19841984- show: false,
19851985- backgroundColor: getSystemThemeBackgroundColor(),
19861986- key: 'background-core',
19871987- webPreferences: {
19881988- preload: preloadPath,
19891989- session: profileSession,
19901990- }
19911991- };
19971997+ const win = await initCore({ tilePreloadPath });
1992199819931993- // Create the background window
19941994- const win = new BrowserWindow(winPrefs);
19951995- win.loadURL(WEB_CORE_ADDRESS);
19991999+ // Keep the local handle in sync so `getBackgroundWindow()` returns
20002000+ // the same reference even while test code that reads it early hasn't
20012001+ // yet switched to the core-glue accessor.
20022002+ backgroundWindow = win;
1996200319971997- // Forward console logs from background window
19981998- win.webContents.on('console-message', (event) => {
19991999- DEBUG && console.log(`[core] ${event.message}`);
20042004+ // Register in the session registry with the legacy address — other
20052005+ // code paths use `WEB_CORE_ADDRESS` to identify the core window
20062006+ // (e.g. `closeOrHideWindow` refuses to close it, `countVisibleWindows`
20072007+ // excludes it). `key: 'background-core'` preserves the v1 winPrefs.key
20082008+ // so any `findWindowByKey(WEB_CORE_ADDRESS, 'background-core')`
20092009+ // lookups continue to work.
20102010+ const systemAddress = getSystemAddress();
20112011+ registerWindow(win.id, systemAddress, {
20122012+ key: 'background-core',
20132013+ address: WEB_CORE_ADDRESS,
20002014 });
2001201520022002- // Setup devtools for the background window (debug mode, but not in tests or headless)
20162016+ // Devtools for the background window (dev-profile only, not tests /
20172017+ // headless). The core renderer was traditionally helpful to inspect
20182018+ // for command registration + pubsub traffic while debugging; keeping
20192019+ // the same affordance post-v2.
20032020 if (config.isDev && !isTestProfile() && !isHeadless()) {
20042021 win.webContents.openDevTools({ mode: 'detach', activate: false });
20052022 }
2006202320072007- // Add to window manager
20082008- registerWindow(win.id, systemAddress, { ...winPrefs, address: WEB_CORE_ADDRESS });
20092009-20102010- // NOTE: No ESC handler for background window - it should never be closed
20112011-20122012- // Set up handlers for windows opened from the background window
20132013- win.webContents.setWindowOpenHandler((details) => {
20142014- DEBUG && console.log('Background window opening child window:', details.url);
20152015-20162016- // Parse window features into options
20172017- const featuresMap: Record<string, unknown> = {};
20182018- if (details.features) {
20192019- details.features.split(',')
20202020- .map(entry => entry.split('='))
20212021- .forEach(([key, value]) => {
20222022- let parsedValue: unknown = value;
20232023- // Convert string booleans to actual booleans
20242024- if (value === 'true') parsedValue = true;
20252025- else if (value === 'false') parsedValue = false;
20262026- // Convert numeric values to numbers
20272027- else if (!isNaN(Number(value)) && value.trim() !== '') {
20282028- parsedValue = parseInt(value, 10);
20292029- }
20302030- featuresMap[key] = parsedValue;
20312031- });
20322032- }
20332033-20342034- DEBUG && console.log('Parsed features map:', featuresMap);
20352035-20362036- // Check if window with this key already exists
20372037- if (featuresMap.key) {
20382038- const existingWindow = findWindowByKey(WEB_CORE_ADDRESS, featuresMap.key as string);
20392039- if (existingWindow) {
20402040- DEBUG && console.log('Reusing existing window with key:', featuresMap.key);
20412041- if (!isHeadless()) {
20422042- existingWindow.window.show();
20432043- }
20442044- return { action: 'deny' as const };
20452045- }
20462046- }
20472047-20482048- // Determine frame setting based on explicit option or preference
20492049- let frameDefault = false; // Default to frameless if pref not available
20502050- if (featuresMap.frame === undefined) {
20512051- const prefs = getPrefs();
20522052- // hideTitleBar: true means frame: false (no titlebar)
20532053- // hideTitleBar: false means frame: true (show titlebar)
20542054- frameDefault = prefs.hideTitleBar === false;
20552055- }
20562056-20572057- // Prepare browser window options
20582058- // Use profile-specific session for isolation
20592059- const winOptions: Electron.BrowserWindowConstructorOptions = {
20602060- frame: frameDefault, // Default based on hideTitleBar pref
20612061- ...(featuresMap as Electron.BrowserWindowConstructorOptions),
20622062- width: parseInt(String(featuresMap.width)) || APP_DEF_WIDTH,
20632063- height: parseInt(String(featuresMap.height)) || APP_DEF_HEIGHT,
20642064- show: isHeadless() ? false : featuresMap.show !== false,
20652065- // Don't set backgroundColor for transparent windows - it would show through
20662066- backgroundColor: featuresMap.transparent ? undefined : getSystemThemeBackgroundColor(),
20672067- webPreferences: {
20682068- preload: preloadPath,
20692069- session: getProfileSession(),
20702070- }
20712071- };
20722072-20732073- // Make sure position parameters are correctly handled
20742074- if (featuresMap.x !== undefined) {
20752075- winOptions.x = parseInt(String(featuresMap.x));
20762076- }
20772077- if (featuresMap.y !== undefined) {
20782078- winOptions.y = parseInt(String(featuresMap.y));
20792079- }
20802080-20812081- DEBUG && console.log('Background window creating child with options:', winOptions);
20822082-20832083- // Make sure we register browser window created handler to track the new window
20842084- const onCreated = (_e: Electron.Event, newWin: BrowserWindow) => {
20852085- // Check if this is the window we just created
20862086- newWin.webContents.once('did-finish-load', () => {
20872087- const loadedUrl = newWin.webContents.getURL();
20882088- if (loadedUrl === details.url) {
20892089- // Remove the listener
20902090- app.removeListener('browser-window-created', onCreated);
20912091-20922092- // Add the window to our manager with necessary parameters
20932093- registerWindow(newWin.id, WEB_CORE_ADDRESS, {
20942094- ...featuresMap,
20952095- address: details.url,
20962096- modal: featuresMap.modal
20972097- });
20982098-20992099- // Track this load in history
21002100- try {
21012101- trackWindowLoad(details.url, {
21022102- source: (featuresMap.trackingSource as string) || 'background',
21032103- sourceId: (featuresMap.trackingSourceId as string) || '',
21042104- windowType: featuresMap.modal ? 'modal' : 'main',
21052105- title: (newWin.getTitle() !== 'Loading...' ? newWin.getTitle() : '') || '',
21062106- });
21072107- } catch (e) {
21082108- DEBUG && console.log('Failed to track background child window load:', e);
21092109- }
21102110-21112111- // Set initial mode based on URL
21122112- updateModeForNavigation(newWin.id, details.url);
21132113-21142114- // Update item title when page finishes loading (background child windows).
21152115- // Without this, titles stay empty or "Loading..." in history.
21162116- if (details.url.startsWith('http://') || details.url.startsWith('https://')) {
21172117- newWin.webContents.on('page-title-updated', (_event: Electron.Event, title: string) => {
21182118- if (title && title !== 'Loading...') {
21192119- try {
21202120- updateItemTitle(newWin.webContents.getURL() || details.url, title);
21212121- } catch (e) {
21222122- DEBUG && console.log('Failed to update title from page-title-updated:', e);
21232123- }
21242124- }
21252125- });
21262126-21272127- // Capture favicon for background child windows
21282128- newWin.webContents.on('page-favicon-updated', (_event: Electron.Event, favicons: string[]) => {
21292129- const pageUrl = newWin.webContents.getURL() || details.url;
21302130- if (pageUrl && favicons.length > 0) {
21312131- const best = favicons.find(f => f.endsWith('.svg')) ||
21322132- favicons.find(f => f.endsWith('.png')) ||
21332133- favicons[favicons.length - 1];
21342134- try {
21352135- updateItemFavicon(pageUrl, best);
21362136- } catch (e) {
21372137- DEBUG && console.log('Failed to update favicon from page-favicon-updated:', e);
21382138- }
21392139- }
21402140- });
21412141- }
21422142-21432143- // Track in-page navigation within this window
21442144- newWin.webContents.on('did-navigate', (_event: Electron.Event, navUrl: string) => {
21452145- if (navUrl === details.url) return;
21462146- // Update mode for the new URL
21472147- updateModeForNavigation(newWin.id, navUrl);
21482148- try {
21492149- trackWindowLoad(navUrl, {
21502150- source: 'navigation',
21512151- sourceId: '',
21522152- windowType: featuresMap.modal ? 'modal' : 'main',
21532153- title: (newWin.getTitle() !== 'Loading...' ? newWin.getTitle() : '') || '',
21542154- });
21552155- } catch (e) {
21562156- DEBUG && console.log('Failed to track did-navigate:', e);
21572157- }
21582158- });
21592159-21602160- // Add escape key handler
21612161- addEscHandler(newWin);
21622162-21632163- // Track window for display change handling
21642164- trackWindow(newWin);
21652165-21662166- // Set up DevTools if requested
21672167- winDevtoolsConfig(newWin);
21682168-21692169- // Set up modal behavior with delay to avoid focus race condition
21702170- if (featuresMap.modal === true) {
21712171- setTimeout(() => {
21722172- if (!newWin.isDestroyed()) {
21732173- newWin.on('blur', () => {
21742174- DEBUG && console.log('Modal window lost focus:', details.url);
21752175- closeOrHideWindow(newWin.id);
21762176- });
21772177- }
21782178- }, 100);
21792179- }
21802180- }
21812181- });
21822182- };
21832183-21842184- // Start listening for the window creation
21852185- app.on('browser-window-created', onCreated);
21862186-21872187- // Return allow with overridden options
21882188- return {
21892189- action: 'allow' as const,
21902190- overrideBrowserWindowOptions: winOptions
21912191- };
21922192- });
21932193-21942194- backgroundWindow = win;
21952024 return win;
21962025}
2197202620272027+// Legacy `setWindowOpenHandler` child-window path removed in Phase 2.5
20282028+// #1. Under v2, `api.window.open(...)` routes through `tile:window:open`
20292029+// (handled in tile-ipc.ts) and delegates to the centralised window-open
20302030+// IPC handler. Native `window.open()` calls from the core background
20312031+// HTML never happen — the renderer has no `<a target="_blank">` or
20322032+// equivalent. The ~150 lines of `setWindowOpenHandler` logic were dead
20332033+// in practice and are deleted.
20342034+21982035/**
21992036 * Get the background window
22002037 */
22012038export function getBackgroundWindow(): BrowserWindow | null {
22022202- return backgroundWindow;
20392039+ // Prefer the core-glue accessor so callers get a live reference even
20402040+ // if `createBackgroundWindow()` hasn't been called yet in this boot
20412041+ // (e.g. during mid-startup IPC that runs before entry.ts's call).
20422042+ return getCoreBackgroundWindow() ?? backgroundWindow;
22032043}
2204204422052045// ***** External URL Handling *****
+102
backend/electron/tile-preload.cts
···20022002 },
20032003 };
2004200420052005+ // ── App control (trustedBuiltin only) ────────────────────────────────
20062006+ //
20072007+ // v1 preload exposed `api.quit` / `api.restart` (fire-and-forget IPC
20082008+ // sends). The core background renderer (app/index.js) registers
20092009+ // `quit` / `restart` cmd palette commands that invoke these. The
20102010+ // channels are `ipcMain.on` (not .handle), so `api.invoke` cannot
20112011+ // replace them — a dedicated `.send` wrapper is required.
20122012+ api.quit = () => {
20132013+ if (!trustedBuiltin) {
20142014+ console.warn('[tile-preload] api.quit requires trustedBuiltin — ignored');
20152015+ return;
20162016+ }
20172017+ ipcRenderer.send('app-quit', { source: sourceAddress });
20182018+ };
20192019+20202020+ api.restart = () => {
20212021+ if (!trustedBuiltin) {
20222022+ console.warn('[tile-preload] api.restart requires trustedBuiltin — ignored');
20232023+ return;
20242024+ }
20252025+ ipcRenderer.send('app-restart', { source: sourceAddress });
20262026+ };
20272027+20282028+ // ── Theme (trustedBuiltin extensions) ────────────────────────────────
20292029+ //
20302030+ // The strict tile:theme surface only exposes `getInfo` and `onChange`.
20312031+ // Core renderers (app/index.js in particular) need the full v1 theme
20322032+ // surface for the cmd palette theme commands (`theme light`, `theme`,
20332033+ // `theme light here`, etc.). The channels are plain `ipcMain.handle`
20342034+ // entries in `ipc.ts`, so we can expose them here under trustedBuiltin
20352035+ // as thin wrappers. Non-trustedBuiltin tiles continue to use the
20362036+ // strict tile:theme:* surface declared above.
20372037+ if (trustedBuiltin) {
20382038+ (api.theme as Record<string, unknown>).get = () => ipcRenderer.invoke('theme:get');
20392039+ (api.theme as Record<string, unknown>).list = () => ipcRenderer.invoke('theme:list');
20402040+ (api.theme as Record<string, unknown>).getAll = () => ipcRenderer.invoke('theme:getAll');
20412041+ (api.theme as Record<string, unknown>).setTheme = (themeId: string) =>
20422042+ ipcRenderer.invoke('theme:setTheme', themeId);
20432043+ (api.theme as Record<string, unknown>).setColorScheme = (colorScheme: string) =>
20442044+ ipcRenderer.invoke('theme:setColorScheme', colorScheme);
20452045+ (api.theme as Record<string, unknown>).setWindowColorScheme = async (colorScheme: string) => {
20462046+ // Core/test renderers target the "last focused visible window" via
20472047+ // `get-focused-visible-window-id`, not their own hidden
20482048+ // BrowserWindow. Preserves v1 behaviour where the background
20492049+ // iframe ran the theme command on behalf of the user-facing window.
20502050+ let windowId = await ipcRenderer.invoke('get-focused-visible-window-id');
20512051+ if (!windowId) {
20522052+ windowId = await ipcRenderer.invoke('get-window-id');
20532053+ }
20542054+ if (!windowId) {
20552055+ return { success: false, error: 'No visible window to target' };
20562056+ }
20572057+ return ipcRenderer.invoke('theme:setWindowColorScheme', { windowId, colorScheme });
20582058+ };
20592059+ }
20602060+20612061+ // ── Chrome Extensions (trustedBuiltin only) ──────────────────────────
20622062+ //
20632063+ // v1 preload exposed `api.chromeExtensions.*` for the core background
20642064+ // renderer to enumerate bundled chrome extensions + their UI entry
20652065+ // points (popup, options, new-tab, etc.) and register cmd palette
20662066+ // commands for each openable entry. Gated on trustedBuiltin — feature
20672067+ // tiles must not enumerate or launch chrome extensions.
20682068+ api.chromeExtensions = {
20692069+ list: () => {
20702070+ if (!trustedBuiltin) {
20712071+ return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' });
20722072+ }
20732073+ return ipcRenderer.invoke('chrome-ext:list');
20742074+ },
20752075+ enable: (id: string) => {
20762076+ if (!trustedBuiltin) {
20772077+ return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' });
20782078+ }
20792079+ return ipcRenderer.invoke('chrome-ext:enable', { id });
20802080+ },
20812081+ disable: (id: string) => {
20822082+ if (!trustedBuiltin) {
20832083+ return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' });
20842084+ }
20852085+ return ipcRenderer.invoke('chrome-ext:disable', { id });
20862086+ },
20872087+ getStatus: () => {
20882088+ if (!trustedBuiltin) {
20892089+ return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' });
20902090+ }
20912091+ return ipcRenderer.invoke('chrome-ext:getStatus');
20922092+ },
20932093+ getUiEntries: () => {
20942094+ if (!trustedBuiltin) {
20952095+ return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' });
20962096+ }
20972097+ return ipcRenderer.invoke('chrome-ext:getUiEntries');
20982098+ },
20992099+ openPage: (id: string, type: string) => {
21002100+ if (!trustedBuiltin) {
21012101+ return Promise.resolve({ success: false, error: 'api.chromeExtensions requires trustedBuiltin' });
21022102+ }
21032103+ return ipcRenderer.invoke('chrome-ext:openPage', { id, type });
21042104+ },
21052105+ };
21062106+20052107 return api;
20062108}
20072109