experiments in a post-browser web
10
fork

Configure Feed

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

fix(window-targeting): track tile-launched window focus + drop page-host's local Maximize cmd

User report: with a tile (tags, search, etc.) focused in front of a
content window (e.g. example.com), running the maximize command
maximizes the content window behind, not the focused tile.

Two distinct contributing bugs were uncovered:

1. tile-launcher.createTileBrowserWindow created BrowserWindows without
attaching the focus listener that updates lastFocusedVisibleWindowId.
The window-open IPC path attached its own focus listener after
window creation, so tiles opened via api.window.open were tracked
correctly. Windows created directly via launchTile (notably resident
tiles wired up at startup) silently skipped the tracker. When such
a window was focused, the tracker stayed pointed at whatever
non-tile window was focused last.

2. The page-host (app/page/page.js) registered a renderer-side
`Maximize` command (`scope: 'window'`). `scope: 'window'` is only a
UI badge — not a dispatch filter — so the cmd panel's prefix matcher
resolved typed `maximize` input to whichever page-host had
registered that name, calling its local `toggleMaximize()` on its
own `myWindowId` and bypassing both the focus tracker and the IPC
boundary entirely. With example.com being the only page-host, its
command always ran regardless of which window was actually focused.

Fix:

- Add an onWindowFocus hook to configureTileLauncher and call it from
inside createTileBrowserWindow on every focus event. main.ts wires
the hook to a new exported trackOnWindowFocus that runs
trackContentWindowFocus + trackVisibleWindowFocus. The hook receives
the manifest entry's role (e.g. 'workspace', 'overlay'), so role-based
exclusions work even though tile windows aren't yet in windowRegistry.

- Extend trackVisibleWindowFocus to accept an optional knownRole
parameter so tile-launcher callers don't need a registry lookup.

- Promote the focus-tracker log from DEBUG-gated to unconditional.
Wrong-window-targeting bugs are reported by symptom, and seeing the
transitions in stderr makes them diagnosable without re-instrumenting
the user.

- Add an unconditional log in tile:window:maximize that prints the
resolved window id + url, so the user can confirm which window was
actually targeted.

- Drop the page-host's local `Maximize` command registration. Maximize
is owned by the windows feature (`maximize window`), which targets
the focus tracker and crosses the IPC boundary. The page-host keeps
its `page:maximize` pubsub subscription and its navbar dbl-click
handler, both of which intentionally toggle the local
`toggleMaximize()` (legitimate UI affordances scoped to the
page-host).

Regression tests:
- Focusing a real tile window (peek://tags/home.html) in front of a
content window must update getFocusedVisibleWindowId to the tile's
id, not leave it pointing at the content window.
- A search tile opened on top of a content window must update the
tracker even without an explicit api.window.focus call.

+114 -11
+6 -7
app/page/page.js
··· 2324 2324 execute: () => webview.reload() 2325 2325 }); 2326 2326 2327 - api.commands.register({ 2328 - name: 'Maximize', 2329 - description: 'Toggle maximize for the current page', 2330 - scope: 'window', 2331 - execute: () => toggleMaximize() 2332 - }); 2327 + // Note: there is intentionally no local `Maximize` command. Maximize is owned 2328 + // by the windows feature (`maximize window`), which targets the focus tracker 2329 + // and crosses the IPC boundary. A local renderer-side toggle here would 2330 + // match cmd-palette input regardless of which window the user was looking 2331 + // at, and toggle whichever page-host registered the handler. 2333 2332 2334 - // Subscribe to page:maximize pubsub 2333 + // Subscribe to page:maximize pubsub for explicit per-window publishes. 2335 2334 api.subscribe('page:maximize', (msg) => { 2336 2335 if (msg.windowId != null && msg.windowId !== myWindowId) return; 2337 2336 toggleMaximize();
+19 -3
backend/electron/ipc.ts
··· 184 184 * 185 185 * IMPORTANT: Modal windows should NOT call this function, otherwise 186 186 * opening the cmd palette would override the target window. 187 + * 188 + * `knownRole` is an optional override for the role check, supplied by 189 + * callers that have the role in hand without going through the registry 190 + * (e.g. tile-launcher, which creates BrowserWindows that aren't yet in 191 + * `windowRegistry`). 187 192 */ 188 - function trackVisibleWindowFocus(win: BrowserWindow): void { 193 + export function trackVisibleWindowFocus(win: BrowserWindow, knownRole?: string): void { 189 194 const url = win.webContents.getURL(); 190 195 // Exclude internal background windows 191 196 if (url === 'peek://app/background.html') { ··· 198 203 // Exclude overlay windows (e.g., windows switcher) — they should not 199 204 // be tracked as the last focused visible window 200 205 const winInfo = getWindowInfo(win.id); 201 - if (winInfo?.params?.role === 'overlay') { 206 + const role = knownRole ?? (winInfo?.params?.role as string | undefined); 207 + if (role === 'overlay') { 202 208 return; 203 209 } 204 210 // Track extension windows (peek://{id}/...) and web pages 205 211 lastFocusedVisibleWindowId = win.id; 206 - DEBUG && console.log('Updated lastFocusedVisibleWindowId:', lastFocusedVisibleWindowId, url); 212 + // Unconditional — wrong-window targeting bugs (e.g. maximize hitting 213 + // the window behind a focused tile) are reported by symptom; seeing 214 + // the tracker transitions in stderr makes them diagnosable without 215 + // re-instrumenting the user. 216 + console.log(`[focus-tracker] updated lastFocusedVisibleWindowId=${lastFocusedVisibleWindowId} url=${url.slice(0, 80)}`); 217 + } 218 + 219 + /** Exported wrapper for tile-launcher: also runs content-window tracking. */ 220 + export function trackOnWindowFocus(win: BrowserWindow, knownRole: string | undefined): void { 221 + trackContentWindowFocus(win); 222 + trackVisibleWindowFocus(win, knownRole); 207 223 } 208 224 209 225 /**
+2 -1
backend/electron/main.ts
··· 31 31 import { getSystemThemeBackgroundColor } from './windows.js'; 32 32 import { getProfileSession, getPartitionString, getCurrentProfileId } from './session-partition.js'; 33 33 import { getIzuiCoordinator } from './izui-state.js'; 34 - import { clearLastFocusedVisibleWindowId } from './ipc.js'; 34 + import { clearLastFocusedVisibleWindowId, trackOnWindowFocus } from './ipc.js'; 35 35 import type { Placement } from './window-placement.js'; 36 36 37 37 // Configuration ··· 206 206 configureTileLauncher({ 207 207 getProfileSession, 208 208 getSystemThemeBackgroundColor, 209 + onWindowFocus: trackOnWindowFocus, 209 210 }); 210 211 211 212 // Install the load-on-dispatch pre-publish hook. Every subsequent
+9
backend/electron/tile-ipc.ts
··· 2999 2999 if (!win || win.isDestroyed()) { 3000 3000 return { success: false, error: 'Window not found' }; 3001 3001 } 3002 + // Logged unconditionally — wrong-window targeting bugs are reported 3003 + // by symptom ("the window behind got maximized"), and seeing the 3004 + // resolved id + url in production stderr makes it diagnosable 3005 + // without re-instrumenting the user. 3006 + try { 3007 + console.log( 3008 + `[tile:window:maximize] id=${args.id ?? 'sender-fallback'} resolved=${win.id} url=${win.webContents.getURL().slice(0, 80)}` 3009 + ); 3010 + } catch { /* logging must never throw */ } 3002 3011 if (win.isMaximized()) { 3003 3012 win.unmaximize(); 3004 3013 } else {
+24
backend/electron/tile-launcher.ts
··· 80 80 // null and the baseline helpers fall back cleanly. 81 81 let _getProfileSession: (() => Electron.Session) | null = null; 82 82 let _getSystemThemeBackgroundColor: (() => string) | null = null; 83 + let _onWindowFocus: ((win: BrowserWindow, role: string | undefined) => void) | null = null; 83 84 84 85 /** 85 86 * Wire tile-launcher up to its electron-side dependencies. Called once ··· 90 91 export function configureTileLauncher(hooks: { 91 92 getProfileSession?: () => Electron.Session; 92 93 getSystemThemeBackgroundColor?: () => string; 94 + /** 95 + * Called on every tile window's `focus` event. Production wiring 96 + * forwards this into `trackVisibleWindowFocus` (and friends) in ipc.ts 97 + * so the per-window-targeting tracker (`getFocusedVisibleWindowId`) 98 + * sees tile-launched windows. Without it, focusing a tile (tags, 99 + * search, etc.) leaves the tracker stale — commands like `maximize` 100 + * then resolve to whatever non-tile window was focused last. 101 + */ 102 + onWindowFocus?: (win: BrowserWindow, role: string | undefined) => void; 93 103 }): void { 94 104 if (hooks.getProfileSession) _getProfileSession = hooks.getProfileSession; 95 105 if (hooks.getSystemThemeBackgroundColor) _getSystemThemeBackgroundColor = hooks.getSystemThemeBackgroundColor; 106 + if (hooks.onWindowFocus) _onWindowFocus = hooks.onWindowFocus; 96 107 } 97 108 98 109 // Short grace period given to tiles to run their `api.onShutdown()` callbacks ··· 307 318 const key = `${tileId}:${entryId}`; 308 319 tileWindows.set(key, win); 309 320 if (manifest) loadedManifests.set(tileId, manifest); 321 + 322 + // Forward focus events so the per-window-targeting tracker 323 + // (`lastFocusedVisibleWindowId` in ipc.ts) sees tile windows. Without 324 + // this, focusing the tags or search tile leaves the tracker pointing 325 + // at whatever non-tile window was focused last (e.g. a content 326 + // page-host), and commands like `maximize` end up on the wrong window. 327 + if (_onWindowFocus) { 328 + const role = entry?.role; 329 + win.on('focus', () => { 330 + if (win.isDestroyed()) return; 331 + _onWindowFocus!(win, role); 332 + }); 333 + } 310 334 311 335 // Forward console messages from the tile's webContents to main-process 312 336 // stdout. Errors/warnings always forwarded; info/debug only under DEBUG.
+54
tests/desktop/window-targeting.spec.ts
··· 168 168 }, { backId: result.backId as number, searchId: result.searchId as number }); 169 169 }); 170 170 171 + test('focusing a tile window updates the tracker (regression: maximize-on-wrong-window)', async () => { 172 + // Bug: with a tile (e.g. tags, search) focused in front of a content 173 + // window, the maximize cmd hit the content window behind. Root cause: 174 + // tile-launcher.createTileBrowserWindow created BrowserWindows 175 + // without attaching the focus listener that updates 176 + // lastFocusedVisibleWindowId — so focusing a tile-launched window 177 + // never updated the tracker, leaving stale state from whatever 178 + // window-open path window was focused last. 179 + // 180 + // Production wires the listener via configureTileLauncher's 181 + // onWindowFocus hook. This test focuses a real tile window and 182 + // asserts the tracker reflects it. 183 + const result = await bgWindow.evaluate(async () => { 184 + const api = (window as any).app; 185 + 186 + // Open a content window first, focus it, so the tracker has a 187 + // non-tile baseline. 188 + const back = await api.window.open('about:blank', { 189 + role: 'workspace', 190 + width: 600, 191 + height: 400, 192 + key: 'targeting-tile-back', 193 + }); 194 + if (!back.success) return { stage: 'open back', error: back.error }; 195 + await api.window.focus(back.id); 196 + await new Promise(r => setTimeout(r, 200)); 197 + 198 + // Open a tile (tags-home is a workspace tile that goes through 199 + // the tile-launcher createTileBrowserWindow path). 200 + const tile = await api.window.open('peek://tags/home.html', { 201 + role: 'workspace', 202 + width: 800, 203 + height: 600, 204 + key: 'tags-home', 205 + }); 206 + if (!tile.success) return { stage: 'open tile', error: tile.error }; 207 + await api.window.focus(tile.id); 208 + await new Promise(r => setTimeout(r, 250)); 209 + 210 + const tracked = await api.window.getFocusedVisibleWindowId(); 211 + return { backId: back.id, tileId: tile.id, tracked }; 212 + }); 213 + 214 + expect(result.error).toBeUndefined(); 215 + expect(result.tracked).toBe(result.tileId); 216 + expect(result.tracked).not.toBe(result.backId); 217 + 218 + await bgWindow.evaluate(async (ids: { backId: number; tileId: number }) => { 219 + const api = (window as any).app; 220 + await api.window.close(ids.backId); 221 + await api.window.close(ids.tileId); 222 + }, { backId: result.backId as number, tileId: result.tileId as number }); 223 + }); 224 + 171 225 test('setWindowColorScheme returns success with windowId', async () => { 172 226 // Test that setWindowColorScheme works and returns expected data 173 227 const result = await bgWindow.evaluate(async () => {