feat(tiles): unify tile window creation (Phase D)
Phase D of the tile-lifecycle rewrite. Every tile BrowserWindow now
goes through one function, `createTileBrowserWindow`, whether it was
triggered by a lazy command (launchTile) or a renderer-side
`api.window.open()` call (window-open IPC). This kills the divergence
class that surfaced the entities-tile-with-an-OS-menubar bug: there's
no longer a "second place" where tile BrowserWindows get built with
different defaults.
- `tile-launcher.ts`:
- New `createTileBrowserWindow(spec)` is the canonical constructor.
Applies baseline chrome (`autoHideMenuBar: true`, `session:
profileSession`, `backgroundColor: system-theme`, sandbox /
contextIsolation / nodeIntegration / preload /
additionalArguments) merged with manifest-entry windowHints then
caller overrides in fixed precedence. Registers in `tileWindows`,
hooks console-message forwarding, renders-process-gone cleanup,
and per-entry pubsub unsubscribe on close.
- `launchTile` is now a thin wrapper: resolve entry + grant +
token → `createTileBrowserWindow` → `loadURL`. Same public
signature, same return shape.
- New `configureTileLauncher({ getProfileSession,
getSystemThemeBackgroundColor })` hook. Both values live on
modules that transitively `import { BrowserWindow }` from
'electron' as ESM, so tile-launcher can't import them directly
without crashing unit tests run under ELECTRON_RUN_AS_NODE=1
(where electron's named ESM exports are empty). The hook is
called once from main.ts::initialize at app startup; unit tests
never set it and the helper falls back to defaults cleanly
because they stub the launcher entirely.
- Per-entry unsubscribe on close/crash (`unsubscribeAll(peek://
{tileId}/{entryId})`) replaces the old per-prefix cleanup
(`unsubscribeAllByPrefix(peek://{tileId}/)`). A tile's window-
entry closing should never wipe its sibling background-entry's
pubsub subscriptions; matches the behavior
`registerTrustedBuiltinWindow` already had.
- `ipc.ts` window-open: when the URL resolves to a real v2 feature
tile (`getTileWebPreferencesForUrl` returned non-null AND the
handler didn't synthesize a trustedBuiltin token AND it's not a
canvas page host), delegate BrowserWindow construction to
`createTileBrowserWindow`. The caller-provided position / modal /
skipTaskbar / transparent flags thread through as
`browserWindowOverrides`; caller-provided `webPreferences` extras
thread through as `webPreferencesOverrides` (after stripping the
fields the helper owns canonically). The subsequent
`registerTrustedBuiltinWindow` call is gated off for this branch —
the helper already registered.
- `main.ts::initialize`: wires the tile-launcher hooks with the real
electron-side getters before any feature or window work.
Deliberately out of scope: canvas page hosts, trustedBuiltin special
cases (settings, diagnostic, hud-overlay, cmd-ui, about:blank,
datastore-viewer), and plain web pages still go through the direct
`new BrowserWindow(winOptions)` path in ipc.ts. They're not v2
feature tiles; unifying them is a separate cleanup.
Tests: 2250/2250 unit + 238/238 Playwright (desktop + desktop-serial).