name: pup-user-foreground description: DEV skill (source-only) - THE doctrine for the user-raise vs pup-demotion fight. John clicks a pup taskbar button and the window must come up INSTANTLY and STAY up; pup's parks and re-asserts must never yank it back down. Revisited ~20 times before this was written; read this BEFORE touching any park, re-assert, z-order, foreground, or occlusion code, and BEFORE adding ANY new demotion path.

The user's taskbar click always wins

The single most-revisited behavior in pup's history. This file exists because we relitigated it about twenty times (2026-06 through 2026-08). The contract, the mechanism, and the bans below are settled. Do not re-derive them; do not weaken them.

The contract

  1. pup windows open in the BACKGROUND and never steal focus.
  2. But the user must be able to raise any pup window INSTANTLY by clicking its taskbar button, with zero delay and zero cooperation from pup.
  3. And once the USER raises a window, pup must NEVER fight them for it. Not once, not "just one re-assert", never.

Why pup demotes windows at all (the legitimate enemies)

  • The first cross-origin paint can RAISE a fresh Chrome window.
  • Chrome's PDF viewer SELF-ACTIVATES the window when a PDF loads (proven live: a 5-PDF window popped over John's work while pup's own log said "backgrounded").
  • After an OS restart, Windows restores session-recovered Chrome windows FOREGROUND.
  • Post-restart foreground reassignment can hand a brand-new window the foreground with no user intent.

Hence the machinery: the launch park, the +2.5s re-assert, the post-nav re-assert (+1.2s retry, +3s timer). Every one of those is aimed at CHROME's self-raises, and every one of them is a loaded gun pointed at the user's click.

The doctrine (current, settled)

  1. Background means BEHIND, never hidden. Park = on-screen, full work-area size, z-BOTTOM (never off-screen, never minimized). A taskbar click then reveals a correctly-placed window with no help from pup (v1.9.133/134, after the "click the flash, see nothing" bug).
  2. User intent is read from the OS, atomically, inside the demotion primitive. GetForegroundWindow() == our hwnd AND GetLastInputInfo idle < 5s means a HUMAN raised it (a keyboard/mouse was active). A programmatic self-raise happens with idle hands. This check lives INSIDE osBottomUnlessForeground (v2.0.26), in the same pass as the demotion, because the session-level flags LAG the click by seconds - guarding only on flags is exactly how the re-asserts kept yanking John's window back down.
  3. force=true means "override CHROME", never "override the USER". Force exists for the PDF pop and launch reassignment. It must not bypass the recent-input check. If you add a new demotion call with force, it inherits the recent-input check automatically by going through the shared primitive - so GO THROUGH THE SHARED PRIMITIVE.
  4. On user-foreground: stop everything and remember. The caller stops immediately (no retry pass, no ab fallback) and stamps session._userActiveAt = Date.now(), which makes userIsUsingWindow() true for 10 minutes - and EVERY demotion path checks that first (v1.9.181, after the unguarded-demotion audit of 2026-07-29: "pup windows still don't behave well" was exactly unguarded demotions).
  5. Every demotion path checks userIsUsingWindow(session) before acting. The full list as of v2.0.26: parkSessionWindowDirect, reassertBottomAfterNav (both passes), the kickoffPark +2.5s re-assert, healOffscreenWindow, the unstick monitor, brand/park sweeps, recovery re-park (sf.foreground persisted flag). Adding a demotion that skips this check is a bug by definition.
  6. The AI raising a window is a separate, GATED thing. foreground:true requires foregroundReason quoting the user's ask. A user-raise (observed) and an AI-raise (granted) both set the same protective flags.

The history that hardened each rule (do not re-learn these live)

Era Lesson
v1.8.53 CDP document.hasFocus() LIES for CDP-created windows - never use it as the user signal. OS GetForegroundWindow is truth.
v1.9.133/134 Parking OFF-screen made taskbar clicks reveal nothing. Background = z-bottom ON-screen, always.
v1.9.137 10px inset on placement so a deliberate park is visually distinct from an accident.
v1.9.181 Granted-raise + observed-user-foreground flags, 10-min window, checked by every demotion. Unguarded demotions were the root of "pup windows still don't behave well".
v1.9.201/202 The re-assert passes exist because PDFs self-activate AFTER the first park; do not delete them - they are why force exists.
v1.9.292 osIdleMs introduced to tell John's click from a JS raise.
v2.0.11 (reverted) Placement-first parking during load FAILED: the session is not registered until after navigation, so early parks have no hwnd and retreat. Do not retry that shape without moving session registration first.
v2.0.26 The force-flag fight: re-asserts with force demoted a window John had JUST raised, because flag guards lag. Fix = recent-input check inside the primitive, same pass, even under force.

Bans (each one shipped and hurt)

  • No demotion path outside the shared primitive / without the userIsUsingWindow check.
  • No off-screen or minimize-based backgrounding of a window the user can see on the taskbar.
  • No re-park retry LOOPS (the playbook ban): one pass + the scheduled re-asserts, then stop.
  • No trusting session flags alone for "is the user using this" at demotion time - flags lag; the OS check rides in the demotion pass itself.

ab 2.0.16 migration note

The PS implementations are being replaced by ab-native verbs (desktop_get_foreground, desktop_user_idle_ms, desktop_set_window_state {state:'bottom'}, desktop_clear_window_attention). THE HEURISTIC MUST SURVIVE THE MIGRATION: foreground check + idle check + demotion should happen within tens of ms of each other, and the user-foreground result must still short-circuit callers and stamp _userActiveAt. If ab ever grows a combined "bottom-unless-user-foreground" verb, prefer it - one atomic call beats three.

The test that must always pass

  1. Open a background pup window; let all parks/re-asserts settle (~5s).
  2. A HUMAN clicks its taskbar button. The window comes up instantly.
  3. It STAYS up: through the next navigate, overlay refresh, and sweep, for 10+ minutes. Log must show user-foreground — hands off, never a bottomed for that window.
  4. Counter-test: a local PDF opened in a background window still gets demoted (idle hands), so the PDF protection is intact.