← Commit history

Capture the final window playbook: EnumWindows real-window resolution, deactivate-then-park order, short input-aware re-park, and the minimize-breaks-screenshots dead-end

John Lauer ·845f2b418b · ·parent f56330f
1 file changed +31−10
dev-skills/pup-bridge-dev/SKILL.md+31−10
@@ -240,11 +240,18 @@ R1 never visually pop / cover the user's work · R2 the window is reachable (tas    `--window-position=-32000,-32000` (NOT `--start-maximized`; strip it when `!foreground`). The window is    created far past any monitor, so nothing ever appears on screen during startup. CDP renders regardless    of position, so screenshots/eval/record all still work.-2. **Park ON-SCREEN at the bottom of the z-order** → satisfies R2 + R3. After CDP connects, move it back-   on-screen sized to the monitor at the very back:-   `SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, GetSystemMetrics(0), GetSystemMetrics(1), SWP_NOACTIVATE)`.-   Fully behind the user's windows (invisible) but it has a real taskbar button to click. (Leaving it-   off-screen breaks R2 — the taskbar button "restores" it off-screen where you can't see it.)+2. **Park ON-SCREEN at the bottom of the z-order** → satisfies R2 + R3. On-screen-occluded (behind the+   user's windows, taskbar-clickable) is the ONLY resting state that keeps R3 — see the minimize dead-end.+   TWO details that took blood to get right:+   - **Resolve the REAL window by ENUMERATION, not `MainWindowHandle`.** `Get-Process(pid).MainWindowHandle`+     returns a HIDDEN Chrome helper window on some boxes → `SetWindowPos` moved a phantom, the real window+     stayed off-screen, and the "backgrounded" self-check passed against the phantom (a FALSE positive).+     Enumerate top-level windows and take the largest VISIBLE `Chrome_WidgetWin_1` owned by the pid/children+     (`WinBgP::BestWindow`). This was THE "I click the taskbar and nothing shows" bug (1.8.46).+   - **Deactivate FIRST, THEN move on-screen** (order matters). If you move it on-screen while Chrome still+     has it foreground, it flashes on TOP for a moment (an active window stays topmost until deactivated) =+     the "I saw it open" pop. So: hand focus back (below) while it is still OFF-SCREEN, and only then+     `SetWindowPos(pup, HWND_BOTTOM, 0, 0, scrW, scrH, SWP_NOACTIVATE)` — now it drops behind, invisible. 3. **Hand keyboard focus back via `AttachThreadInput`** → satisfies R4, and is the crux. Plain    `SetForegroundWindow(usersWindow)` is BLOCKED by Windows' foreground lock. The workaround:    - snapshot the user's window with `GetForegroundWindow()` **before** spawning the browser;@@ -252,9 +259,13 @@ R1 never visually pop / cover the user's work · R2 the window is reachable (tas      window's thread via `GetWindowThreadProcessId`), which shares the foreground privilege → then      `SetForegroundWindow(usersWindow)` + `BringWindowToTop(usersWindow)` succeeds → detach with      `AttachThreadInput(..., false)`.-   - Re-run the whole background+handback a few times over ~2.5s (600/1400/2500ms) because the browser-     re-grabs during startup. The initial `priorForeground` is stashed on `browser._priorForeground` so-     the after-sizing re-background can restore too.+   - **Keep the re-park window SHORT (~1.2s: 300/700/1200ms), not 2.5s+.** The re-passes only exist to+     counter Chrome re-grabbing foreground during startup. A LONG/aggressive loop FIGHTS the user: they+     click the window to view it and the next pass yanks it back to the background. After ~1.2s pup must+     NEVER auto-re-park. WITHIN the window, use `GetLastInputInfo`: pup-foreground + a physical input in the+     last ~1.5s = the USER (Chrome self-raise is programmatic, no input) → bring the window on-screen+     (centered ~80%) + latch `browser._userForeground` + cancel remaining passes; else re-park. See the+     "user↔AI balance" section below — this is the behavior John cares most about. 4. **Run all this PowerShell HIDDEN** → satisfies R5. Use `execFileSync('powershell.exe', [...args],    {windowsHide:true})` + `-WindowStyle Hidden` (see `runPsHidden`/`runPsHiddenAsync`), NEVER    `execSync('powershell …')` — the latter goes through cmd.exe and flashes a console window. (A cmd blip@@ -274,8 +285,18 @@ must clear it first.   before you lower it = the visible pop. This is the naive version; off-screen launch replaces it. - **`SetForegroundWindow(usersWindow)` alone** → silently no-ops (foreground lock). You MUST use the   `AttachThreadInput` attach/detach dance.-- **Minimize-and-leave (`SW_MINIMIZE`, don't re-show)** → drops focus cleanly BUT a minimized window stops-  compositing, so `Page.captureScreenshot` **times out** (breaks R3). Rejected.+- **Minimize as the background state** → tried TWICE now (SW_MINIMIZE 1.8.26-era, and full+  `SetWindowPlacement`-restore-rect minimize in 1.8.47). It's SO tempting because minimizing hands focus+  back for free and a taskbar click restores-and-stays (no re-park loop). But a minimized window reports+  `document.visibilityState === 'hidden'` and Chrome SUSPENDS its renderer, so `Page.captureScreenshot`+  **hangs ~27s then errors** — EVEN WITH `--disable-features=CalculateNativeWinOcclusion` ++  `--disable-backgrounding-occluded-windows` (those cover OCCLUDED windows, not MINIMIZED). Breaks R3.+  **REJECTED — do not try minimize again.** On-screen-occluded is the only state that both hides the window+  AND keeps it rendering. (`osMinimizeToBackground` may still be in the tree, unused — do not wire it back.)+- **`MainWindowHandle` to resolve the window** → returns a hidden Chrome helper on some boxes; you park a+  phantom and the real window never moves. Enumerate `Chrome_WidgetWin_1` instead (technique #2).+- **A long/aggressive re-park retry loop** → fights the user's click (yanks the window back). Keep it short+  (~1.2s) and input-aware (technique #3). - **`WS_EX_NOACTIVATE` on the window** → Chrome self-activates anyway (so it doesn't fix R4), AND a   non-activatable window can't be brought up by a taskbar click (breaks R2). Rejected. - **Minimize→re-show loop as the focus drop** → on an on-screen window the minimize/restore is a visible