···101101} from './session-partition.js';
102102import { saveSessionSnapshot, restoreSessionSnapshot, readSessionCrashState, markSessionDirty, startAutosaveTimer, stopAutosaveTimer, updateAutosaveInterval } from './session.js';
103103import { initDisplayWatcher, cleanupDisplayWatcher } from './display-watcher.js';
104104+import { getTileWebPreferencesForWebviewUrl } from './tile-launcher.js';
105105+import { getLazyTileManifest } from './tile-lazy.js';
106106+import { getTilePreloadPath } from './config.js';
104107105108// Early diagnostic logging for URL handling (writes to /tmp/ to avoid userData path issues)
106109try {
···168171}
169172170173// Inject preload into <webview> elements that load peek:// URLs.
171171-// This gives internal widget pages (e.g., HUD widgets) access to window.app API
172172-// while leaving external web content webviews untouched.
174174+// This gives internal widget pages (e.g., HUD widgets, widget-demo cards)
175175+// access to window.app API while leaving external web content webviews untouched.
176176+//
177177+// Two paths (Phase 2.5 #2 — webview preload migration):
178178+// 1. v2 tiles: if the URL resolves to a registered v2 tile manifest,
179179+// use tile-preload.cjs with capability-token argv (the same shape
180180+// v2 BrowserWindows get via webPreferences.additionalArguments).
181181+// Webview children inherit the parent tile's grant via
182182+// `getTileWebPreferencesForWebviewUrl`.
183183+// 2. v1 fallback: any peek:// URL whose tile-id isn't a v2 tile (e.g.
184184+// `peek://ext/hud/widgets/...` while HUD is still v1) keeps the
185185+// legacy preload.js. This will go away when HUD is migrated.
173186//
174187// Also inject Proton Pass webauthn.js into http/https webviews for passkey support.
175188// The script overrides navigator.credentials.create/get in the main world.
176189app.on('web-contents-created', (_event, contents) => {
177190 contents.on('will-attach-webview', (_wvEvent, webPreferences, params) => {
178191 if (params.src && params.src.startsWith('peek://')) {
179179- webPreferences.preload = preloadPath;
180180- DEBUG && console.log(`[webview] Injecting preload for peek:// webview: ${params.src}`);
192192+ const tilePreloadPath = getTilePreloadPath();
193193+ const tileWebPrefs = tilePreloadPath
194194+ ? getTileWebPreferencesForWebviewUrl(params.src, tilePreloadPath, getLazyTileManifest)
195195+ : null;
196196+ if (tileWebPrefs) {
197197+ webPreferences.preload = tileWebPrefs.preload;
198198+ // Cast: Electron's WebPreferences type for webviews omits
199199+ // additionalArguments in some versions, but the runtime accepts
200200+ // it (set in BrowserWindow webPrefs the same way).
201201+ (webPreferences as { additionalArguments?: string[] }).additionalArguments =
202202+ tileWebPrefs.additionalArguments;
203203+ DEBUG && console.log(
204204+ `[webview] Injecting v2 tile-preload for ${params.src} (tile=${tileWebPrefs.tileId}, entry=${tileWebPrefs.entryId})`
205205+ );
206206+ } else {
207207+ webPreferences.preload = preloadPath;
208208+ DEBUG && console.log(`[webview] Injecting v1 preload for peek:// webview: ${params.src}`);
209209+ }
181210 }
182211 });
183212
+75
backend/electron/tile-launcher.ts
···475475}
476476477477/**
478478+ * Resolve a peek:// URL to v2 tile web preferences for a `<webview>` child.
479479+ *
480480+ * Like `getTileWebPreferencesForUrl`, but treats child URLs that don't
481481+ * correspond to a declared tile entry as belonging to the parent tile's
482482+ * grant. This is the loader path for HTML assets a tile loads inside a
483483+ * `<webview>` element (e.g. `widget-demo`'s widget cards loading
484484+ * `peek://ext/widget-demo/widgets/clock.html` — `widgets/clock.html` is
485485+ * not declared as its own tile entry, but logically belongs to
486486+ * `widget-demo`).
487487+ *
488488+ * Resolution rules:
489489+ * - URL must be peek://
490490+ * - Tile-id (URL host or first path segment under `peek://ext/`) must
491491+ * match a registered v2 manifest
492492+ * - If a tile entry's `url` matches the path, mint a token for that
493493+ * specific entry (same as `getTileWebPreferencesForUrl`)
494494+ * - Otherwise, fall back to the manifest's first tile entry — webview
495495+ * children inherit the parent tile's full capability grant
496496+ *
497497+ * Returns null when the URL isn't peek:// or the tile-id isn't a v2
498498+ * tile. Callers should fall back to the legacy preload in that case
499499+ * (e.g. for v1 extensions like HUD).
500500+ */
501501+export function getTileWebPreferencesForWebviewUrl(
502502+ url: string,
503503+ tilePreloadPath: string,
504504+ lookupLazyManifest: (tileId: string) => TileManifestV2 | null,
505505+): { preload: string; additionalArguments: string[]; tileId: string; entryId: string } | null {
506506+ let host: string;
507507+ let pathPart: string;
508508+ try {
509509+ const parsed = new URL(url);
510510+ if (parsed.protocol !== 'peek:') return null;
511511+ const cleanPath = parsed.pathname.replace(/^\//, '');
512512+ if (parsed.host === 'ext') {
513513+ const segments = cleanPath.split('/');
514514+ if (segments.length < 2) return null;
515515+ host = segments[0];
516516+ pathPart = segments.slice(1).join('/');
517517+ } else {
518518+ host = parsed.host;
519519+ pathPart = cleanPath;
520520+ }
521521+ } catch {
522522+ return null;
523523+ }
524524+525525+ const manifest = getTileManifest(host) || lookupLazyManifest(host);
526526+ if (!manifest) return null;
527527+ if (!manifest.tiles || manifest.tiles.length === 0) return null;
528528+529529+ // Prefer an exact entry match, fall back to the first entry for
530530+ // webview children that aren't declared as standalone tile entries.
531531+ const entry = manifest.tiles.find(t => t.url === pathPart) || manifest.tiles[0];
532532+533533+ const grant = resolveCapabilities(
534534+ manifest.id,
535535+ manifest.capabilities,
536536+ manifest.builtin === true,
537537+ );
538538+ const token = generateToken(manifest.id, entry.id, grant);
539539+540540+ return {
541541+ preload: tilePreloadPath,
542542+ additionalArguments: [
543543+ `--tile-id=${manifest.id}`,
544544+ `--tile-entry=${entry.id}`,
545545+ `--tile-token=${token}`,
546546+ ],
547547+ tileId: manifest.id,
548548+ entryId: entry.id,
549549+ };
550550+}
551551+552552+/**
478553 * Send the `tile:shutdown` IPC to a single tile window, wait a short grace
479554 * period for its `api.onShutdown()` callbacks to run, and then close the
480555 * BrowserWindow.