experiments in a post-browser web
10
fork

Configure Feed

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

fix(theme): setWindowColorScheme window resolution in headless (Cat 3)

The trustedBuiltin override of api.theme.setWindowColorScheme in
tile-preload.cts was guarded by if (trustedBuiltin) at buildAPI()
time, when the flag is always false (validateToken() runs
asynchronously from api.initialize()). The guard meant the wrapper
was never installed — callers hit the base function with a missing
windowId and failed.

The wrapper is now installed unconditionally. It dispatches on arg
shape: (colorScheme) resolves the target via the tracker hint,
(windowId, colorScheme) preserves the legacy explicit-target form.
Non-trusted callers are still rejected by the strict
tile:theme:setWindowColorScheme handler.

The strict handler itself now has a resolution chain so it can target
the right window even when the lastFocusedVisibleWindowId tracker
is stale (e.g. headless show:false windows never emit focus
events). Order: explicit windowId -> tracked id -> getFocusedWindow ->
enumerate non-destroyed, non-background, non-test-fixture, focusable
BrowserWindows (most-recent by id).

Fixes smoke tests 4547 / 4590 / 4650.

Validation: yarn test:grep 'Window Targeting' -> 3 passed (9.5s).

+111 -22
+70 -4
backend/electron/tile-ipc.ts
··· 3495 3495 // Strict counterpart of the legacy `theme:setWindowColorScheme` channel. 3496 3496 // Sets color scheme for a specific window only (does not affect global setting). 3497 3497 // Requires trustedBuiltin. 3498 + // 3499 + // windowId resolution: callers may pass an explicit windowId, or omit it 3500 + // (null/undefined) and let the handler resolve. Resolution order: 3501 + // 1. Explicit windowId (if positive finite number and window exists) 3502 + // 2. lastFocusedVisibleWindowId tracked by ipc.ts 3503 + // 3. BrowserWindow.getFocusedWindow() 3504 + // 4. First non-destroyed, non-test-fixture, non-background BrowserWindow 3505 + // that was not created as modal (by skipping `focusable:false` and 3506 + // `background.html` URLs). This last branch is the headless-test 3507 + // fallback — in headless mode all windows have `show:false` so 3508 + // `isVisible()` is false and no focus events fire. 3498 3509 ipcMain.handle('tile:theme:setWindowColorScheme', (_event, args: { 3499 3510 token: string; 3500 - windowId: number; 3511 + windowId?: number | null; 3501 3512 colorScheme: string; 3502 3513 }) => { 3503 3514 if (!args?.token) return { success: false, error: 'Invalid token' }; ··· 3508 3519 return { success: false, error: 'trustedBuiltin required for tile:theme:setWindowColorScheme' }; 3509 3520 } 3510 3521 3511 - const { windowId, colorScheme } = args; 3522 + const { colorScheme } = args; 3512 3523 if (!['system', 'light', 'dark', 'global'].includes(colorScheme)) { 3513 3524 return { success: false, error: 'Invalid color scheme' }; 3514 3525 } 3515 3526 3516 - const win = BrowserWindow.fromId(windowId); 3527 + // Resolve windowId. If caller passed a positive finite number, try it 3528 + // first. Otherwise walk the fallback chain. 3529 + let resolvedId: number | null = null; 3530 + const explicitId = typeof args.windowId === 'number' && Number.isFinite(args.windowId) && args.windowId > 0 3531 + ? args.windowId 3532 + : null; 3533 + if (explicitId !== null) { 3534 + const explicitWin = BrowserWindow.fromId(explicitId); 3535 + if (explicitWin && !explicitWin.isDestroyed()) { 3536 + resolvedId = explicitId; 3537 + } 3538 + } 3539 + 3540 + if (resolvedId === null) { 3541 + const tracked = getLastFocusedVisibleWindowId(); 3542 + if (tracked != null) { 3543 + const trackedWin = BrowserWindow.fromId(tracked); 3544 + if (trackedWin && !trackedWin.isDestroyed()) { 3545 + resolvedId = tracked; 3546 + } 3547 + } 3548 + } 3549 + 3550 + if (resolvedId === null) { 3551 + const focused = BrowserWindow.getFocusedWindow(); 3552 + if (focused && !focused.isDestroyed()) { 3553 + resolvedId = focused.id; 3554 + } 3555 + } 3556 + 3557 + if (resolvedId === null) { 3558 + // Headless fallback: find a plausible target by filtering out the 3559 + // test fixture (focusable:false) and the core background renderer 3560 + // (url === peek://app/background.html). Take the highest-numbered 3561 + // matching window as a proxy for "most recently created". 3562 + const candidates = BrowserWindow.getAllWindows().filter((w) => { 3563 + if (w.isDestroyed()) return false; 3564 + if (!w.isFocusable()) return false; 3565 + try { 3566 + const wurl = w.webContents.getURL(); 3567 + if (wurl === 'peek://app/background.html') return false; 3568 + if (wurl.startsWith('peek://test/')) return false; 3569 + } catch { /* ignore */ } 3570 + return true; 3571 + }); 3572 + if (candidates.length > 0) { 3573 + candidates.sort((a, b) => b.id - a.id); 3574 + resolvedId = candidates[0].id; 3575 + } 3576 + } 3577 + 3578 + if (resolvedId === null) { 3579 + return { success: false, error: 'No visible window to target' }; 3580 + } 3581 + 3582 + const win = BrowserWindow.fromId(resolvedId); 3517 3583 if (!win || win.isDestroyed()) { 3518 3584 return { success: false, error: 'Window not found' }; 3519 3585 } 3520 3586 3521 3587 win.webContents.send('theme:windowChanged', { colorScheme }); 3522 - return { success: true, windowId, colorScheme }; 3588 + return { success: true, windowId: resolvedId, colorScheme }; 3523 3589 }); 3524 3590 3525 3591 // ── tile:theme:setTheme ────────────────────────────────────────────
+41 -18
backend/electron/tile-preload.cts
··· 1979 1979 // so we keep a thin trustedBuiltin wrapper here that resolves the ID 1980 1980 // before forwarding to the strict channel. All other theme methods route 1981 1981 // through the base api.theme.* implementations above (tile:theme:*). 1982 - if (trustedBuiltin) { 1983 - (api.theme as Record<string, unknown>).setWindowColorScheme = async (colorScheme: string) => { 1984 - // Core/test renderers target the "last focused visible window" via 1985 - // `get-focused-visible-window-id`, not their own hidden 1986 - // BrowserWindow. Preserves v1 behaviour where the background 1987 - // iframe ran the theme command on behalf of the user-facing window. 1988 - let windowId = await ipcRenderer.invoke('get-focused-visible-window-id'); 1989 - if (!windowId) { 1990 - windowId = await ipcRenderer.invoke('tile:window:get-id', { token: tileToken }); 1991 - if (windowId && typeof windowId === 'object' && 'id' in windowId) { 1992 - windowId = (windowId as { id: number }).id; 1993 - } 1982 + // Install the override unconditionally (the install-time `trustedBuiltin` 1983 + // flag is still `false` here — buildAPI runs synchronously before 1984 + // api.initialize() flips it). The handler itself runs at call time, by 1985 + // which point `trustedBuiltin` reflects the validated grant. Non-trusted 1986 + // callers are still rejected by the strict tile:theme:setWindowColorScheme 1987 + // handler which enforces `grant.trustedBuiltin`. 1988 + (api.theme as Record<string, unknown>).setWindowColorScheme = async ( 1989 + windowIdOrColorScheme: number | string, 1990 + maybeColorScheme?: string, 1991 + ) => { 1992 + // Core/test renderers target the "last focused visible window" via 1993 + // `get-focused-visible-window-id`, not their own hidden 1994 + // BrowserWindow. Preserves v1 behaviour where the background 1995 + // iframe ran the theme command on behalf of the user-facing window. 1996 + // 1997 + // Support two call shapes: 1998 + // setWindowColorScheme(colorScheme) — trusted, resolve via tracker 1999 + // setWindowColorScheme(windowId, colorScheme) — legacy explicit target 2000 + let windowId: number | null = null; 2001 + let colorScheme: string; 2002 + if (typeof windowIdOrColorScheme === 'number' && typeof maybeColorScheme === 'string') { 2003 + windowId = windowIdOrColorScheme; 2004 + colorScheme = maybeColorScheme; 2005 + } else if (typeof windowIdOrColorScheme === 'string') { 2006 + colorScheme = windowIdOrColorScheme; 2007 + } else { 2008 + return { success: false, error: 'Invalid arguments to setWindowColorScheme' }; 2009 + } 2010 + 2011 + if (windowId == null) { 2012 + // Pass the tracked id as a hint. The strict handler has its own 2013 + // resolution chain (tracked id → getFocusedWindow → headless 2014 + // enumeration) so it can still resolve a target when the hint is 2015 + // null — critical for headless tests where `show:false` windows 2016 + // never emit focus events and the tracker may be stale. 2017 + const hint = await ipcRenderer.invoke('get-focused-visible-window-id'); 2018 + if (typeof hint === 'number' && hint > 0) { 2019 + windowId = hint; 1994 2020 } 1995 - if (!windowId) { 1996 - return { success: false, error: 'No visible window to target' }; 1997 - } 1998 - return ipcRenderer.invoke('tile:theme:setWindowColorScheme', { token: tileToken, windowId, colorScheme }); 1999 - }; 2000 - } 2021 + } 2022 + return ipcRenderer.invoke('tile:theme:setWindowColorScheme', { token: tileToken, windowId, colorScheme }); 2023 + }; 2001 2024 2002 2025 // ── Chrome Extensions (trustedBuiltin only) ────────────────────────── 2003 2026 //