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 resolveFaviconRichanalyzeIconBytes 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 triggersframenavigatedscheduleOverlayRefresh (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 applyAppOverlayresolveSessionHwndByBounds). 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.