---
name: pup-window-targeting
description: THE LAW for how pup finds a session's OS window for any Bridge window verb - ALWAYS by its resolved HANDLE (hwnd), NEVER by title. Title lookup is unreliable (titleTag is off by default and pages rewrite their own <title>) and it has independently broken the overlay paint, the park, the taskbar flash, the AUMID stamp, AND the jump list - the same bug, over and over. READ THIS before adding or touching ANY chrome.adCommand('desktop_*') call that acts on a window, or before adding a desktop_find_window resolver.
---

# pup window targeting: hwnd, never title (the recurring bug, killed)

John, across ONE session, caught this same class of bug break FOUR separate features, each of which I
"fixed" one at a time before he snapped: "how many times did you say you couldn't look up a window by
title and it messed up your algorithm... you keep duct-taping it instead of a permanent solution." He
was right. This file is the permanent solution so it never regresses.

## The disease

Every one of these subsystems, independently, tried to find a session's OS window by matching
`(ai-thread: <sid>)` in its title (via `desktop_find_window`/`titleContains`), and every one FAILED
the moment the title wasn't there:

- **Overlay paint** - windows came back with no badge / `hwnd=None`.
- **Park (z-bottom + place)** - windows stranded off-screen or never bottomed.
- **Taskbar flash** - "did not match the window (title changed?)".
- **AUMID stamp** - `"No visible window with title containing (ai-thread: pup-dashboard..."` → the
  dashboard window never stamped, so only SOME windows went teal (the mismatch John kept seeing).
- **Jump list** - `updateWikiJumplist` resolved hwnd by title, got null, so `payload.hwnd` was never
  set and the list never committed ("i turned on jump lists and none of them work").

Why title is fundamentally unreliable, and will NEVER be the mechanism:
1. **`titleTag` is OFF by default** - John turned it off on purpose; he does not want `(ai-thread: X)`
   decorating his window titles. So most windows carry NO tag to match.
2. **Pages rewrite `document.title`** as they load (SPAs constantly). Even a transient tag gets wiped
   before a find runs. The old re-stamping MutationObserver that fought this caused its own freezes.

## THE LAW

**Every AD window verb targets the window by its resolved HWND, via `windowTarget()`. Never by
title.** `titleContains` survives ONLY as the cold fallback inside `windowTarget()` itself.

```js
// returns { hwnd } (validated, read-only) or, only if we truly have no handle, { titleContains }
const tgt = await windowTarget(session, sessionId);
await chrome.adCommand('desktop_taskbar',            { ...tgt, overlay: {...} });
await chrome.adCommand('desktop_set_window_state',   { ...tgt, state: 'bottom', force: true });
await chrome.adCommand('desktop_set_window_identity',{ ...tgt, appId, ... });   // STAMP
await chrome.adCommand('desktop_set_window_jumplist',{ hwnd: session._hwnd, appId, tasks });
```

`windowTarget()` resolves + validates `session._hwnd` (via `hwndBelongsToPup`), read-only, and NEVER
touches window geometry. Spread it into the verb args. That is the whole rule.

### When you add a new window verb
Spread `...(await windowTarget(session, sessionId))`. Do NOT write a fresh
`desktop_find_window({ titleContains })` to "get the hwnd" - that is the exact anti-pattern that keeps
coming back. If you catch yourself typing `titleContains: \`(${TITLE_LABEL}` in anything other than the
`windowTarget` fallback, STOP.

## Where the handle comes from (so the fallback stays cold)

Resolution is READ-ONLY. It never resizes or moves a window (see the wiggle grave below). Priority:

1. **Cached `session._hwnd`**, validated by `hwndBelongsToPup` (rejects a stale/recycled/foreign
   handle - e.g. one now owned by Edge or another profile's process).
2. **Birth-time capture** - `createPageInNewWindow` births each window at a UNIQUE off-screen position
   and reads its handle THERE, while it is off-screen and small, before park maximizes it. That is the
   only moment a same-profile window is unambiguous. Stashed on `page.__pupHwnd`, trusted first.
3. **Persisted handle** - `_hwnd` is written to the session file and restored on EVERY adoption path
   (`recoverSessions` AND `rescanProfile`). A Chrome window keeps the SAME hwnd across a bridge
   respawn, so a recovered window restores its handle instead of re-resolving.
4. **Sole-window-of-a-profile** - if a profile's process owns exactly one top-level Chrome frame, that
   IS the window (works even maximized).
5. If none of the above: **leave it BARE** (no overlay). Bare > wrong, forever (John's Edge-overlay
   rule). NEVER guess.

## ⚰️ The width-wiggle is DEAD. Do not resurrect it.

For months, same-profile windows (which share the identical parked rect) were disambiguated by
temporarily SETTING A UNIQUE WIDTH via CDP, enumerating, and restoring. It is GONE (v1.9.308) because:
- It never worked on **maximized** windows (CDP can't resize a maximized window) - it was matching by
  luck/collision the whole time.
- Its DPI-scale guessing was promiscuous and **collapsed multiple windows onto one handle**.
- When a restore failed it left a window **stuck narrow** - John: "why did you resize this window so
  weird? what the fuck?" Deforming the user's window is never acceptable.

There is no invisible way to geometry-probe an on-screen window. Birth-time capture + persistence is
the answer. If you think you need the wiggle, you need birth-time capture instead.

## One-line summary
Target windows by handle through `windowTarget()`, resolve the handle read-only (birth-time + persist,
never resize), leave bare when unsure, and never, ever look a window up by its title again.
