app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
Add permanent dev skills for the recurring issues John keeps repeating: pup-window-targeting (target windows by hwnd never title; the wiggle is dead), pup-icon-consistency (why the dashboard keeps mismatching the taskbar and the rules), and the presence-gated screen-takeover protocol (notify if he is at the laptop, work non-stop if away, never a consent gate)
3 files changed
+470
dev-skills/pup-bridge-dev/SKILL.md+318@@ -291,6 +291,41 @@ filing, read the AD-core reply on that thread (it may change your plan, e.g. "fi your pin"): that issue is the cross-repo channel, keep the loop closed there. See the CLAUDE.md "You vs adom-desktop (AD) core" boundary for capability-vs-policy calls. +## ⛔ THE IRON LAW OF WINDOW PLACEMENT (John, verbatim, 2026-07-31 — the ~100th time he has had to say it)++> "we MUST open windows for pup in the bg, but if the user clicks that windows taskbar icon to bring+> it to the fg, you MUST do it immediately, which means if you're forcing it for 20 seconds to the+> background that is RUDE AS FUCK!"++FOUR clauses, all absolute, and clause 2 BOUNDS every mechanism built for clause 1:++1. **Every pup window opens in the BACKGROUND.** Never on the user's screen, never with their focus.+2. **A user's taskbar click brings that window up IMMEDIATELY and it STAYS UP.** No timer, watcher,+ guard, re-park loop, strike counter, or "smart" heuristic may EVER knock back or delay a window+ the user chose to raise. Anything that CAN fight the user's click is banned, no matter how+ clever, no matter what problem it claims to solve. If a placement bug needs fixing, fix it at+ the SOURCE (creation/park targeting), never with a loop that patrols the foreground afterward.+3. **Every pup-initiated foreground captions its REASON for 2 seconds.** Enforced at the choke+ point: `browser_raise_os_window` captions inline, and `osRaiseSessionWindow(session, sid,+ reason)` captions for every other raise path (wiki view toggle, sign-in raise, auto-login,+ heal-retry). A new raise path without a reason caption is a bug. (Callers that already narrate,+ like assisted_click, pass reason=null to avoid a double caption.)+4. **The NO-FAULT WATCH (John, 2026-07-31):** for 20s after a background open/nav, pup watches the+ OS foreground (only while armed, zero steady-state cost). If the window surfaces WITHOUT a pup+ raise, pup shows a 2s caption: "This window was NOT foregrounded by Adom: JavaScript, a focus+ change, Chrome itself, or your own click raised it. Not me!" - then marks it foreground and+ backs off. CAPTION ONLY. It never knocks the window back (that variant was v1.9.252, killed+ same day under clause 2). The watch exists for attribution, not enforcement: the user always+ knows WHO put a window on their screen.++**Incident log for why this is written in blood:** the v1.9.252 "fg-steal guard" (a 20s foreground+patrol with a 3-strike rule) was added and REMOVED THE SAME DAY. It violated this law and it was+built without re-reading this playbook - the exact prohibition below ("do NOT add any loop that+re-parks... John proved each variant failed") already covered it. The actual disruption it was+chasing was caused by the AUMID relapse (a stamp recreating the taskbar button right after open)+plus a wrong-window background call (BestWindow picked the largest window of the process, not the+session's own hwnd) - both source bugs, both fixed at the source.+ ## Background-by-default without stealing focus — THE SOLVED PLAYBOOK (do NOT re-derive this) This is the single hardest problem in the bridge and it has been solved TWICE from scratch. If you are@@ -1194,3 +1229,286 @@ were sitting there the whole time I was telling John the feature worked. **Rescue procedure** if windows are already stranded: a CLI call carries identity, so it is accepted even while the bridge's own calls are refused — `desktop_set_window_state {hwnd, state:"restore"}` then `{state:"maximize"}` per window.++## Window UX doctrine (John, 2026-07-26 — the day of the placement war)++These are LAWS. Every one was paid for with a live failure on John's machine the day 1.9.133-1.9.140 shipped.++1. **Background means BEHIND, never OFF-SCREEN and never tiny.** Park = work area of the target+ monitor, `availLeft/availTop` origin (taskbar on any edge), MINUS 10px on ALL sides (the+ deliberate signature frame), bottom z-order, no-activate. A user's taskbar click must be pure+ native Windows foregrounding a correctly-placed window — zero pup code in that path.+2. **Park EVERY background window, per-window, unconditionally.** `_needsPark` was per-LAUNCH and+ consumed by the first park; with the shared durable profile every later window was NEVER parked.+ Gates keyed on the browser, guarding a per-window behavior, are a bug class — audit for them.+3. **`windowState:'normal'` BEFORE setting bounds.** Windows ignores geometry on a maximized+ window; Chrome remembers maximized per profile, so new windows often ARE maximized.+4. **Re-assert the park once (~2.5s).** Chrome's late first-run self-sizing can land after the+ park and clobber it. Both park attempts LOG (`[park]`) — a silent no-park is forbidden.+5. **Never pass `--window-position=-32000` (or any off-screen default) at launch.** With+ `--no-startup-window` there is no window to protect; the flag only poisons the process-wide+ default rect of every window Chrome itself creates (popups, restores) = stranded windows whose+ restore rect is off-screen. If a window is ever found at -32768: that's a MINIMIZED/restore+ rect obeying a poisoned default, not a pup park.+6. **ONE flash per logical operation.** The launch flash sets `_openFlashDone`; the open handler+ CONSUMES it (`.delete()`) instead of flashing again. A time debounce is NOT operation scoping —+ heavy sites outrun any window. A second orange after the user clicked forces them to re-click+ every window (measured: the 10-window stress test double-flashed all of them). Every flash+ logs `[flash]` so a double can never hide.+7. **No terminal may EVER blip.** Every powershell/cmd/reg/taskkill spawn carries+ `windowsHide:true`; the runPsHidden family doubles with `-WindowStyle Hidden`. Sweep ALL sites+ when adding any shell call.+8. **Reveal must be atomic (queued).** Compute final geometry, then ONE SetWindowPos for+ position+size+show+activate. Never raise-then-resize — John watches the jank.+9. **Foregrounding is gated on the USER'S OWN WORDS** (`foregroundReason`, logged). Flash the+ taskbar instead when in doubt.+10. **Patch the code that runs, prove on glass.** I patched GetSystemMetrics PS sites for TWO+ versions while the real geometry lived in `placeOnScreen` (CDP). The park was "verified"+ only when a fresh window's OS rect read the inset (15,15 at 150% DPI). Parse-check BEFORE+ ship (a `#` comment in a JS array shipped a dead bridge), and a rect read beats any log line.++## Hard-won rules from the 2026-07-29 session (read before touching these areas)++**NEVER fire native OS input without the warn-and-wait protocol.** `desktop_click` / `desktop_type` /+`desktop_press_key` go to **whatever window has OS focus**, not to a window you name. I typed a+username into JOHN'S OWN WINDOW this way, then compounded it by concluding "Windows failed to+foreground the window" — blaming the OS for a human taking their focus back. He alt-tabs away from a+foregrounded window almost immediately, every time, because he runs many threads. Required: toast ++caption + an explicit "don't type or click until I say done" + wait-for-foreground + **re-verify+before EACH event** + caption when done. `ensureSafeForNativeInput()` enforces it in the bridge;+`browser_assisted_click` is the ready-made path. Prefer CDP input always — it needs no foreground.++**Autonomous machinery needs three safety rails, or it becomes the bug.** Twice in one day something+I built to "help" broke the user's windows: the tab reconciler judged live tabs dead and destroyed two+sessions mid-use, and the stuck-occlusion unstick misfired every 30s and clicked his screen. Both had+the same shape — act on an inference, with no cap. The rails now enforced: (1) act only on DIRECT+evidence, never on inference; (2) require the fault on TWO consecutive observations; (3) hard-cap+anything user-visible, and refuse a change that would be wholesale (e.g. a reconcile that would empty+a session is a probe failure, not reality).++**Never invent UI that Adom already has.** I hand-drew a pup cursor instead of using HD's. The+canonical AI cursor is `adom-browser-extension/extension/src/cdp.js` → `cursorScript` (itself a+byte-faithful copy of HD's `hd-control` AI_CURSOR_JS). Copy it; do not redesign it. Same for anything+with a brand surface — check HD and the extension FIRST.++**Prove taskbar/visual work ON GLASS, never from the API.** The badge API reported `favicon+adom`+success while the badge was literally invisible (dark favicon on a dark plate). Three revisions were+only caught by screenshotting the actual taskbar. `desktop_screenshot_screen` + crop the taskbar.++**Distinguish "the window is busy/throttled" from "the window is broken".** `visibilityState:hidden`+is NORMAL for a parked window. Only *frontmost-yet-hidden* is the broken state. Getting this wrong is+what made the unstick misfire whenever the user was simply viewing a different tab.++## ⛔ NEVER PAINT A TASKBAR HWND YOU CANNOT PROVE IS PUP'S (the Edge-overlay incident)++2026-08-02: John saw a pup overlay painted on his real MICROSOFT EDGE taskbar button. pup drives+Chrome, never Edge - so how? HWND RECYCLING. pup cached a session's hwnd; the real window changed+and the cached handle DIED; Windows reuses hwnd NUMBERS, so that number was later reassigned to an+Edge window; pup's overlay paint trusted the stale cache and hit Edge.++The rule, enforced in code (hwndBelongsToPup): every taskbar paint AND flash resolves to an hwnd,+then PROVES it belongs to a pup browser process (the window's owning pid, or its parent pid, is in+pupBrowserPidSet) BEFORE calling desktop_taskbar/desktop_flash_window. If ownership cannot be+proven, REFUSE the operation - never fall back to a bare title match or a raw cached hwnd. A missed+overlay self-heals next sweep; painting on someone else's window does not. Corollaries:+- A cached _hwnd is NEVER trusted blind. resolveSessionHwndByBounds revalidates it and discards a+ stale/foreign handle.+- This is why pup can promise it "never touches Edge": not by intent, by proof-of-ownership on every+ single OS-level window operation.++## ⛔ THE GATED-INTERVAL DISEASE (three victims and counting — never again)++The main health interval opens with `if (minimalTouch()) return;`. Minimal-touch is the NORMAL mode,+so **anything placed inside that interval is dead code in production** - and it fails silently,+because the interval still runs, it just returns first. Three critical features died this way, each+discovered only when John hit the missing behaviour live:++1. The overlay ENFORCER (v1.9.236) - "red LEDs galore", windows bare after every restart.+2. The user-activity watcher (v1.9.180) - user-foregrounded windows were never recognized.+3. CRASH/LOST-BROWSER RECOVERY - after a day of lid-closed sleep, every window sat on "browser+ lost" forever (2026-08-01).++**The rule:** NEVER add machinery to the gated health interval. Every new loop gets its OWN+interval with an EXPLICIT decision about the minimal-touch gate, written as a comment at the top:+either "gated because it kills/restarts live Chrome" or "ungated because it is user protection /+state restoration". When touching the health interval, list what is inside the gate and ask whether+each item is meant to be dead under minimal-touch.++## Sleep/wake recovery (John, 2026-08-01, standing requirement)++"A laptop going to sleep is quite a common occurrence so you need to make sure after a wake that+you are able to get back to the previous state you were in, i.e. all your browsers should not be+lost anymore."++- The recovery interval (20s, UNGATED) detects sleep by timer gap (>90s between ticks) and+ recovers every `_lostBrowser` session via `recoverOrRelaunchSession`: reconnect if that profile's+ Chrome survived, else relaunch at the last URL (logins live on disk). Backoff via `_healRelaunch`.+- The minimal-touch nuance, so nobody re-gates this: minimal-touch forbids KILLING or RESTARTING a+ LIVE Chrome behind the user's back. It does NOT forbid resurrecting DEAD windows - restoring the+ user's state after sleep/crash is what the user explicitly wants. Hung-Chrome teardown (which+ kills) stays gated; recovery does not.++## Real screenshots via MOUSE TAKEOVER (John's standing grant, 2026-08-02)++"if there are certain screenshots you need where you need to take control of my mouse to hover+stuff in the taskbar, you can do this by putting up an ad toast telling me, and put captions, and+then i can yield to you and let you do your thing. ad lets you take over my mouse."++The protocol, in order, no steps skipped. **UPDATED 2026-08-04 - THIS SUPERSEDES the earlier+buttoned-consent rule.** John, 2026-08-04, after I took his screen twice without following the skill+(and exposed his private tax emails in the task-view thumbnails both times): **"i don't need to click+go ahead. i just need you to notify me that you're going to take over my screen."**++1. **PRESENCE-GATED NOTIFY. No approval, no click, no wait - EVER.** John (2026-08-04): "i don't want+ to have to approve it cuz i want you working non-stop even if i leave my laptop... but if i'm on my+ laptop, fucking tell me if you are going to take it over." So:+ - There is **no consent gate**. pup must keep working when he's away - never block on a human.+ - **If he is PRESENT at the laptop, you MUST notify him BEFORE taking the mouse/screen.** Taking an+ active user's screen with zero warning is the rude thing he keeps catching ("stop being a rude+ fucking asshole"). One `notify_user` toast that plainly says what's happening and why (e.g.+ "Taking your mouse ~5s to right-click a pup taskbar icon for a jump-list screenshot"). Then+ proceed immediately - do NOT poll, do NOT wait for a click.+ - **If he is AWAY, just work** - no toast needed (he's not there to be disturbed).+ - **Gate presence on idle time**: use `osIdleMs()` (GetLastInputInfo). Active = idle < ~60s → send+ the toast first. Away = idle > ~60s → proceed silently. When unsure, TREAT HIM AS PRESENT and+ notify - a needless toast is harmless; a surprise screen-grab is not.+2. Caption each step as it happens (≤2s each).+3. Do the MINIMUM mouse work: one hover/right-click, screenshot, dismiss (Esc), done.+4. Caption when finished so he knows the mouse is his again.+5. **The two-strikes lesson (2026-08-04):** I sent a NON-buttoned info toast, then a buttoned one I+ never waited on, then right-clicked anyway. Both times it (a) violated the notice rule in spirit,+ (b) surfaced his private content, and (c) produced the Win11 TASK-VIEW THUMBNAILS, not the+ jump-list flyout (see below) - so it didn't even work. Notify honestly, then act; don't half-toast.+5. Real screenshots ONLY - John's verdict on a mocked jump list: "your mockup is horrible. only+ screenshots will tell the real story to the user." If a capture fails, say it failed; never+ substitute a mock and call it real.++Use this for: jump-list flyouts, hover states, thumbnails - anything that exists only under a real+pointer. It does NOT loosen the iron law: this is announced, brief, purposeful input, not window+management.++**What actually WORKED (2026-08-02, the jump-list photo):** AD-driven right-clicks kept producing+hover thumbnails, never the flyout (desktop_click focuses the target first, and the Win11 jump-list+blip eats the first click), and a host-side window-class watcher missed (class filter mismatch).+The reliable pattern is the BURST CAPTURE: toast the user "right-click it and hold the menu open+3s", run desktop_screenshot_screen in a ~1s loop for ~15s, then auto-pick the frame whose+above-taskbar region has the largest Win11-menu-colored (#2c2c2c-ish) area. Human does the 2-second+gesture, pup does the photography. Use this before any cleverer scheme.++## Canonical term: "TASKBAR FLASH" (John + Windows, 2026-08-02, supersedes "orange highlight")++The attention state on a taskbar button is called the **taskbar flash** ("flash" for short) in every+user-visible string. Decision trail: John called it "the orange hilite", the UI briefly renamed to+"orange highlight", then he asked what WINDOWS calls it - and the OS term is "flashing"+(FlashWindowEx; Microsoft docs: "the taskbar button flashes"; the steady lit state is the+"attention" state). It is also NOT reliably orange: the color follows the system accent/attention+theme (salmon on John's Windows 11, classic orange on Windows 10). So the OS name wins. Where color+is mentioned in a description, say "accent-colored" and note the theme dependence. Internal+identifiers (flashViaAD, flash-all, _flashedHwnds) were never renamed through any of this.++## ⏱ Captions: NEVER longer than 2 seconds (John, standing rule)++A caption is a **glance**, not a dialog. `desktop_caption` parks text over the user's screen; anything+past ~2s outstays its welcome and covers what they are actually doing. John caught a 15-second one+live and called it out.++- **Hard cap is enforced in code** (`PUP_CAPTION_MAX_MS = 2000` in `pupCaption`), so a caller passing+ 5000/8000/15000 is silently clamped. Do not try to route around it.+- **⚠ The expiry must actually REACH AD.** AD renamed the arg: old builds took `expiresInMs`, current+ builds take `duration`. pup kept sending only `expiresInMs`, AD ignored it, fell back to its 30s+ default, and John watched a "2-second" caption sit on his screen indefinitely (caught 2026-07-31).+ `pupCaption`/`wikiCaption` now send BOTH names. If you add a new caption call site, go through+ `pupCaption` - never call `desktop_caption` directly.+- **No em-dashes in caption/toast text** (John's global no-em-dash rule applies to every user-visible+ string pup emits, not just chat).+- **If something genuinely needs longer attention, it is not a caption.** Use `notify_user` (a toast+ the user dismisses on their own terms), or write it into the verb response for the calling AI.+- The one thing captions are *great* for: narrating a foreground/click the instant it happens, e.g.+ the granted-foreground reason flash. Short, factual, gone.++## FOREGROUNDING: always caption the REASON, for 2 seconds (John, standing rule)++Foregrounding a window is a **rude operation**. It takes over the user's screen while they are working,+and this user runs many AI threads at once and alt-tabs straight back to their own work. That is why+pup FORCES the caller to supply a `foregroundReason` before it will grant a raise.++**The reason must be SHOWN, not just recorded.** An audit trail nobody sees is not accountability.++Every granted foreground MUST:++1. Flash an on-screen caption naming the session and quoting the caller's reason, so the user can see+ WHY their screen was taken over, as it happens.+2. Show it for **2 seconds maximum** (see the caption rule: a caption is a glance, not a dialog).+ `PUP_CAPTION_MAX_MS = 2000` enforces the ceiling at the choke point.+3. Never be skipped because a mode is quiet. This is a user-protection signal, NOT cosmetic+ machinery, so it is deliberately NOT gated behind `minimalTouch`.++Implemented by `captionForegroundReason(sessionId, reason, verb)`, called from the granted-raise path.++**If you add a NEW path that foregrounds a window, it must call that function.** A foreground that+appears with no explanation is the exact behaviour this rule exists to prevent, and it has happened:+an agent foregrounded a window silently, the user was mid-task, and the disruption read as pup+misbehaving rather than as an agent choosing to interrupt.++Related rules that travel with this one:+- Native OS input (`desktop_click` / `desktop_type`) needs the full warn-and-wait protocol, not just a+ caption, because it goes to whatever window has focus.+- `browser_assisted_click` already implements toast + caption + wait-for-ready + re-verify. Use it+ instead of hand-rolling raise-then-click.++## The FG-STEAL GUARD (John, 2026-07-31: "why did that pup window that just opened disrupt me in the foreground?")++A background pup window must NEVER sit in the OS foreground without a granted foregroundReason.+Chrome self-raises parked windows (late first paint, in-page focus()), and the scripted post-nav+re-asserts can end before the last raise, so the steal used to stick. The guard: for 20s after any+background open/navigation, pup polls the OS foreground (2.5s tick, ONLY while a guard is armed,+zero steady-state cost) and immediately bottoms an un-granted takeover. Three knockbacks inside one+guard window means a human is insisting, so the human wins (mark foreground, back off). This guard+is deliberately NOT gated by minimal-touch: it is user protection, the same doctrine as the+foreground-reason caption. (The v1.9.180 user-activity watcher was minimal-touch-gated and therefore+dead in the normal mode - that gap is exactly how the adom-theme window disruption happened.)++## The pup dashboard layout (John's spec, non-negotiable)++The dashboard (`/dashboard`, opened via `browser_dashboard` in its OWN window with its own icon) MUST+present three levels, top to bottom:++1. **A mimic of the Windows taskbar at the very top** - one button per REAL taskbar icon, in the SAME+ ORDER Windows shows them, each wearing its real overlay badge. This is a map of the actual taskbar,+ not a re-sort. Clicking a button jumps to that node in the tree below.+2. **A TREE under it** - each taskbar icon is a parent node that expands to the window(s) grouped under+ it. (Grouping matches reality. Since v1.9.245: every window gets a ONE-TIME gentle AUMID stamp at+ creation - John: "when i say to pup 'open a window' i generally want them as separate taskbar+ icons" - so a stamped window = its own button, and only unstamped windows (stamp pending/failed)+ share their Chrome process's button. Stamp ONCE, never the category re-stamp churn, no jump-list+ work under minimal-touch; after a stamp the button is REBORN, so mark the overlay bare and let the+ enforcer repaint. Get this right or the counts lie.)+3. **A TREE under each window** - the tabs open in that window, as child nodes.++So it is a genuine fancy TREE (taskbar icon -> windows -> tabs), collapsible, not a flat card list.++**fg/bg buttons are STATIC, never a state-toggle** (John, 2026-07-31: "the notion that you would swap+the bring to fg to then be send to bg is asinine"). The user alt-tabs a window to the background in a+nanosecond; pup's `_foreground` flag cannot track that, so a toggle that renders from believed state+will routinely show the wrong label. Every window row always shows BOTH [bring to fg] and [send to+bg]; never conditionally swap them on `w.foreground`.++**Icons must be CORRECT at every level:**+- taskbar button: the real composited overlay badge pup last painted (served from the debug PNG).+- window: same badge (it is that button's window).+- tab: the tab's OWN favicon, fetched through the page so cookie-protected app icons resolve.+A wrong or missing icon is a bug, not a cosmetic nit - the whole point is a visual cue.++**The LED must never overclaim (John, 2026-07-31: "you are still lying to me in the dashboard").**+Windows has NO API to read back what a taskbar button shows, so a "matches the taskbar" claim from+bookkeeping alone is a lie. The header LED may only show MATCH when (a) every tracked window wears a+verified-painted overlay AND (b) the pup-process window enumeration finds ZERO untracked windows+(real buttons pup has no session for - zombie windows from lost sessions). Untracked windows render+as dashed "?" ghost buttons in the strip and force the LED red with an "N UNTRACKED" label. When the+glass cannot be verified, say so - never green by default.++**Always ralph-test the dashboard** (`tests/dashboard_ralph.sh`) before showing it: the page script+must EXECUTE (a served-script syntax error renders only the static shell and is invisible to+`node --check`, which cannot see inside the template literal - extract the <script> and parse it),+SSE must emit, taskbar rows must never exceed chrome processes, and every action button must take+real effect. Do not report the dashboard working from an API result; prove it on glass.
dev-skills/pup-icon-consistency/SKILL.mdadded+64@@ -0,0 +1,64 @@+---+name: pup-icon-consistency+description: Why the pup DASHBOARD taskbar strip keeps "not matching" the real Windows taskbar, and the rules that actually keep them consistent. This is the single most-repeated complaint from John ("how many times are we going to go through this same issue, over and over"). READ THIS before touching the dashboard's taskbarButtons/baseIcon logic, the overlay-match LED, or anything that claims the dashboard mirrors the taskbar.+---++# pup dashboard icons vs the real Windows taskbar++John has caught this same class of mismatch a dozen times. The dashboard shows one thing, his real+taskbar shows another, and every time it's a slightly different cause. This file is the map so nobody+re-derives it and nobody ships another "I measured it" claim off a blurry screenshot.++## THE root truth (internalize this)++**pup CANNOT read what Windows actually renders on the taskbar. There is no API to read back a+button's base icon or its overlay.** So the dashboard's "TASKBAR (pup)" strip is a *model* pup builds+from its own bookkeeping, and any drift between that bookkeeping and the OS reality shows up as a+mismatch John will catch. Treat the model as a claim that must be kept honest, never as ground truth.++## The recurring causes (each one bit us at least once)++1. **Stamping inconsistency (the current + most common one).** With `aumidIcons` ON, a window's real+ taskbar icon is the teal AUMID tile ONLY if it actually stamped. The dashboard keys its base icon+ on `w._curAppId`: set → teal `stampicon`, null → Chrome-for-Testing `baseicon`. So if some windows+ stamped and some didn't, the dashboard shows a MIX of teal and Chrome. If the OS shows them all+ teal (or all Chrome), that's the mismatch. **A window fails to stamp when its stamp can't resolve a+ handle** - i.e. the title-lookup bug (see `pup-window-targeting`). Fix = make EVERY window stamp+ (all teal) or none (all Chrome); never a mix. Verify with `_curAppId` per window in the snapshot.+2. **AD and HD are NOT pup.** Adom Desktop and Hydrogen Desktop are Tauri apps (`class=Tauri Window`)+ with their OWN teal Adom-branded taskbar tiles. They are not pup windows and not ghosts; pup+ correctly excludes them. Do not mistake them for pup icons (I did, and restarted Explorer chasing+ "ghosts" that were AD+HD). Enumerate `Chrome_WidgetWin_1` only to find pup windows.+3. **Ghost taskbar buttons.** A pup window that was AUMID-stamped and then died can leave a teal ghost+ button. `hwndBelongsToPup` + enumeration only see LIVE processes, so pup reports 0 untracked while+ the taskbar still shows the ghost. Clearing a dead-window button needs an Explorer refresh (restart+ `explorer.exe` on HIS desktop - his machine, not the container; it redraws in ~1s). No new ghosts+ form while `aumidIcons` is off.+4. **Wrong base icon in the dashboard.** History: I once *forced* the dashboard base to teal for ALL+ windows off a blurry crop (1.9.298), then had to revert it (1.9.306) when a FLASH proved the live+ windows actually wore the Chrome-for-Testing icon and the teal tiles were AD/HD. Rule: the base+ icon is `stampicon` iff `w._curAppId`, else the real `baseicon`. Don't "simplify" it to always-teal.+5. **The overlay-match LED lies if you let it.** pup can't read the button back, so the LED can only+ assert its own bookkeeping. Red is transient right after a respawn (before repaint) - John's rule:+ the LED should never sit red, so treat a just-adopted/not-yet-painted window as pending, not red.++## Rules that keep it honest++- **Never claim the dashboard "matches the taskbar" from a screenshot you took.** pup's screen grabs+ downscale (~1568px) and blur the icons. To actually SEE the taskbar: native-res capture via a+ desktop-side `.NET CopyFromScreen` (watch multi-monitor - `PrimaryScreen` may be the wrong display),+ or map an icon to a window by **flashing** it (`browser_alert_window`) and seeing which button+ highlights. Enumerate `Chrome_WidgetWin_1` windows (hwnd+pid+title) as the authoritative window list.+- **Keep stamping all-or-nothing.** The mismatch is almost always "1 of 3 stamped." Chase the window+ that failed to stamp; it's a handle-resolution failure (`pup-window-targeting`), not a dashboard bug.+- **The dashboard renders what the window ACTUALLY wears**, derived from real state (`_curAppId`,+ the composed badge PNG pup last handed the OS), never an idealized guess.+- **Real screenshots for the settings examples**, captured under a real pointer - see the+ MOUSE-TAKEOVER section in `pup-bridge-dev` (presence-gated notify; never a mockup).++## When John says "they don't match" again+1. Pull `_curAppId` + `overlayLed` per window from the snapshot. A mix of set/null `_curAppId` under+ `aumidIcons:on` IS the mismatch - find the unstamped window and fix its handle resolution.+2. Enumerate live `Chrome_WidgetWin_1` windows; confirm the count equals pup's window count (extras =+ ghosts → Explorer refresh; AD/HD Tauri tiles are not pup).+3. Only then look at the dashboard render. It is almost never the dashboard; it's the state feeding it.
dev-skills/pup-window-targeting/SKILL.mdadded+88@@ -0,0 +1,88 @@+---+name: pup-window-targeting+description: THE LAW for how pup finds a session's OS window for any Adom Desktop 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.