---
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.
