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

## The favicon-overlay compositor bug (v1.9.326 — John: "icons are STILL wrong")

Even with the model consistent, a pup tile can wear the WRONG or a BARE badge because the composite
dropped the favicon. Two root causes, both fixed in 1.9.326, both worth knowing:

1. **Format judged by URL, not bytes.** The compositor set `isIco` from `src.href.endsWith('.ico')`.
   But `resolveFaviconRich` → `analyzeIconBytes` has ALREADY re-encoded the favicon to PNG, so a site
   whose favicon URL ends in `.ico` (Google Docs' `docs-favicon-2026-v2.ico`, many others) arrived as
   PNG, went down the ICO branch, `icoLargestPng` returned null on PNG bytes, the favicon layer was
   dropped, and the badge rendered as the **bare Adom mark**. FIX: sniff the real magic bytes
   (`00 00 01 00` = ICO; else decode as a normal image). NEVER branch on the href extension.
2. **The generic-glyph fallback set `src` but not `iconPng`.** The composite draws ONLY `iconPng`;
   the "no usable favicon → generic globe" fallback assigned a new SVG to `src` and left `iconPng`
   null, so undecodable favicons ALSO went mark-only. FIX: render the glyph straight into `iconPng`,
   and set `src.href='generic:page'` so `_overlayIsGeneric` (hence the LED) is honest (amber, not a
   lying green).

Debug method that actually works: pull `%TEMP%\pup-badge-debug\<session>.png` — the EXACT bytes pup
handed Windows — and look. If the composed PNG is bare/wrong, it's the compositor (above); if the PNG
is right but the taskbar is wrong, it's delivery/targeting (`pup-window-targeting`). Do NOT diagnose
this from a downscaled taskbar screenshot; crop the native-res `Shell_TrayWnd` capture per-tile.

## The overlay GUARANTEE — why a window ends up BARE, and the three layers that fix it

John, repeatedly: "ensure you ALWAYS have an overlay icon working consistently... every verb that
loads tabs / opens windows / changes pup must trigger a correct overlay AND main-tile analysis." And
with the notch shipped, a bare window is now a VISIBLE HOLE, so this got sharper.

ROOT CAUSE of a bare window (found by tracing, not guessing): `applyAppOverlay` REFUSES to paint when
it can't confirm the window's hwnd belongs to pup — the "Edge guard" (never paint a badge onto a
non-pup taskbar button). So **a missing overlay is almost always an UNRESOLVED HWND**, not a compositor
bug. Anything that churns adoption — a bridge respawn (e.g. the user rebuilding AD in a loop), a
dragged-out tab, sleep/wake — forces hwnd re-resolution and leaves the window transiently bare.

The guarantee is THREE layers (all must stay intact):
1. **Per-event triggers** — `framenavigated` → `scheduleOverlayRefresh` (any nav from any source),
   `MUTATING_VERBS` → refresh, open/close tab+window → refresh. These fire the recompute immediately on
   a real change.
2. **The enforcer backstop** — a 4s sweep (NOT gated on minimalTouch) that repaints ANY bare-or-stuck
   window regardless of which verb did or didn't fire. Bare cooldown 4s. It re-resolves the hwnd each
   attempt (via `applyAppOverlay` → `resolveSessionHwndByBounds`). This is the real guarantee: even if a
   verb forgets to trigger, the window is healed within ~4s. Do NOT slow it back down, and do NOT let it
   `continue` past the overlay paint when a base-tile re-stamp is pending (they are independent AD calls).
3. **The baked notch fallback** — the static category tile has a small Adom mark baked INTO the notch, so
   a window that is momentarily bare shows a tidy branded corner instead of a raw transparent hole. The
   live favicon overlay covers it when present. This makes a transient bare state look intentional, not
   broken — critical because the notch made bareness visible.

If overlays are STILL unreliable after this: the fault is hwnd resolution (`pup-window-targeting`), not
the overlay code. Chase why `resolveSessionHwndByBounds` / the birth-hwnd / persisted-hwnd path fails to
re-confirm the window after adoption — that is the thing that starves the overlay.

## The Adom mark on the overlay is AUMID-CONDITIONAL (John, 2026-08-05)

The composed favicon overlay carries the little Adom mark in its lower-right ONLY when **aumidIcons is
OFF**. Rationale (John): the mark exists to say "this is Adom-controlled." When AUMID is ON, the base
taskbar tile IS the teal Adom category tile — that already says it — so a second mark on the favicon is
redundant. AUMID on → overlay is the CLEAN site favicon, nothing added. AUMID off → favicon + Adom mark
(the base is plain Chrome and needs the branding). The badge composite cache key includes the AUMID
state so it recomposes on toggle. (Related: the baked Adom-mark notch fallback only shows when there is
NO favicon at all, a transient placeholder the live favicon covers — keep it small/central so an opaque
favicon fully hides it and it can't read as a redundant second mark.)

## 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 (`pup_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.
