Skip to content

Update instant-taskbar-thumbnail-previews.wh.cpp - #4981

Open
alchemyyy wants to merge 2 commits into
ramensoftware:mainfrom
alchemyyy:instant-taskbar-thumbnail-previews
Open

Update instant-taskbar-thumbnail-previews.wh.cpp#4981
alchemyyy wants to merge 2 commits into
ramensoftware:mainfrom
alchemyyy:instant-taskbar-thumbnail-previews

Conversation

@alchemyyy

@alchemyyy alchemyyy commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Changelog

If this pull request updates an existing mod, describe the changes below:

  • Added a new feature / value to keep remaining previews open for a configurable delay after closing a window from its thumbnail. This way thumbnail previews won't instantly vanish when the user closes a thumbnail on the edge of a set of them because their cursor isn't immediately over another thumbnail.

  • Tweaked the default close delay to 200ms. I find this provides optimal expected behavior.

Mod authorship

If this pull request introduces a new mod, please complete the section below.

This mod was created by:

    • The submitter, without AI assistance
    • The submitter, with AI assistance
    • Claude
    • ChatGPT
    • Gemini
    • Another AI (please specify):
    • Other (please specify):

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

@alchemyyy

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer

Copy link
Copy Markdown

Thanks for the pull request! This repository uses a two-stage review: an AI review that you run yourself, followed by a human review.

To get started, comment /ai-review. Once you're happy with the result, comment /ready-for-reviewer to hand it over to a human reviewer.

See the pull request review process for the full details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 2, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The change is small and well shaped: the new hook is optional and fails closed, the DismissDelaySource enum is a clean generalization of the old boolean scope, the tagging state is thread_local like the rest of the per-taskbar-thread state, and the settings/README/version updates all line up. One state-management issue:

The close-button tag can survive its transition and re-flavor an unrelated dismissal

g_thumbnailCloseButtonDelayPending is set on every close-button click, but it is only cleared by two events: a subsequent TransitionToFlyoutDismissPendingState (consumes it) or a subsequent TransitionToFlyoutVisiblePendingState (supersedes it). A close-button click that produces neither leaves the tag armed indefinitely.

That is not an exotic path — it's the complement of the case your other mod (taskbar-thumbnail-hover-fix) exists for. Close a thumbnail that is not on the edge, another slides under the pointer, the pointer is still over the flyout, so no dismiss-pending transition fires. The tag stays set. Then:

  • The user eventually moves the pointer off the flyout → that dismissal is a plain pointer-exit, but it consumes the stale tag and gets closeButtonDelayMs (500 ms) instead of closeDelayMs (150 ms).
  • Or the flyout is torn down through CommitDismissFlyout / HideAllHoverFlyouts / ~HoverFlyoutController instead, none of which clear the tag — so it can persist across flyouts and mis-flavor the pointer-exit close of a completely different taskbar item, arbitrarily later.

The effect is bounded (one close is slower than configured), but it means the closeDelayMs setting is silently ignored in a reachable case, which is the kind of thing users report as "the close delay setting doesn't work".

The cheapest robust fix is to bound the tag in time instead of relying on the next transition to consume it, since the close-button dismissal follows the click within a frame or two:

// The dismiss transition caused by thumbnail removal follows the click almost
// immediately; anything later is an unrelated pointer exit.
#define THUMBNAIL_CLOSE_BUTTON_TAG_LIFETIME_MS 1000

thread_local ULONGLONG g_thumbnailCloseButtonClickTick = 0;

void WINAPI TaskItemThumbnailViewOnCloseButtonClicked_Hook(
    void* object,
    const void* sender,
    const void* eventArguments) {
    g_thumbnailCloseButtonClickTick = GetTickCount64();
    TaskItemThumbnailViewOnCloseButtonClicked_Original(
        object, sender, eventArguments);
}

void WINAPI TransitionToFlyoutDismissPendingState_Hook(void* object) {
    ULONGLONG clickTick = g_thumbnailCloseButtonClickTick;
    g_thumbnailCloseButtonClickTick = 0;

    DismissDelaySource source =
        (clickTick && GetTickCount64() - clickTick <=
                          THUMBNAIL_CLOSE_BUTTON_TAG_LIFETIME_MS)
            ? DismissDelaySource::thumbnailCloseButton
            : DismissDelaySource::pointerExit;

    DismissTransitionScope dismissTransitionScope(source);
    TransitionToFlyoutDismissPendingState_Original(object);
}

Optionally also clear it (g_thumbnailCloseButtonClickTick = 0;) at the top of CommitDismissFlyout_Hook, HideAllHoverFlyouts_Hook and HoverFlyoutControllerDestructor_Hook — those are the reliable "this flyout is gone" signals, and they close the cross-flyout leak directly rather than by timeout. Keeping the existing supersede-on-TransitionToFlyoutVisiblePendingState clear is still correct either way.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • DEFAULT_CLOSE_DELAY_MS / DEFAULT_CLOSE_BUTTON_DELAY_MS are dead and duplicate the settings block. LoadSettings() is the first thing Wh_ModInit does, and hooks aren't active until it returns, so no hook can ever observe the static initializers. Their only effect is a second copy of 150 / 500 that can drift out of sync with the YAML defaults. Dropping them (leaving the atomics at MINIMUM_DELAY_MS, as g_hoverDelayTimeSpan still is) removes the drift risk. While you're there, the four #defines would read better as constexpr — same suggestion as last round.

  • The README still has no screenshot or GIF. This was raised on the previous PR and is still open; with three timing settings now, a short GIF showing the show/close/grace behavior would make the mod page a lot easier to understand than the prose. Only i.imgur.com and raw.githubusercontent.com are allowed image hosts. Compare taskbar-thumbnail-reorder.wh.cpp#L31.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Changing closeDelayMs from 1 to 150 silently changes behavior for existing users. Anyone who left the setting at its default (which, for a mod named Instant Taskbar Thumbnail Previews, is probably most of them) will get a 150 ms close delay after the update without touching anything, and there's nothing in the mod UI telling them why. It's your mod and your call, but if you keep it, it's worth a line in the README saying the close delay defaults to a short grace period rather than to instant, so the name doesn't mislead.

  • The grace delay only covers the thumbnail's X button. OnCloseButtonClicked is one of several ways a thumbnail disappears — middle-clicking a thumbnail (if your build supports it), closing the window from its own UI while the flyout is open, or the app exiting on its own all remove a thumbnail without tagging the transition, so the remaining previews still vanish on the normal close delay. Middle-click in particular is a common habit for exactly the "close several in a row" workflow this feature targets. Worth either tagging the same state from whatever handler covers it, or noting the limitation in the $description.

  • The grace period runs unconditionally, including when the pointer leaves for good. Closing a thumbnail and then moving the pointer straight to the desktop keeps the flyout on screen for the full closeButtonDelayMs. That's inherent to implementing this as a MouseHoverTime override, and 500 ms is short enough that it's probably fine — just be aware the setting is a flat lower bound on how long the flyout lingers after any close-button click, not only when the pointer is still nearby.

  • This feature and taskbar-thumbnail-hover-fix now address the same interaction from opposite sides — that mod fixes hover not updating when a thumbnail slides under the pointer, this one fixes the flyout vanishing when nothing slides under it. Since you own both, a sentence in each README pointing at the other would help users who hit one symptom find the other fix.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 2, 2026
@alchemyyy
alchemyyy force-pushed the instant-taskbar-thumbnail-previews branch from 55b65b6 to 5b4a3b6 Compare August 2, 2026 04:42
@alchemyyy

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 2, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The addition is small and well contained: one extra optional symbol hook, no new state that needs tearing down, the new tag is thread_local like the rest of the per-taskbar-thread state, and the CommitActiveHoverFlyoutImmediatelyClearActiveHoverFlyoutController refactor is behaviorally identical to the code it replaces (the guard is always true there). Two things about the new behavior:

1. The 1-second tag lifetime doesn't match what the comment says it's there for

// Removal-triggered dismissal follows the collection event immediately
// Anything later is an unrelated pointer exit
constexpr ULONGLONG THUMBNAIL_REMOVAL_TAG_LIFETIME_MS = 1000;

If a removal-driven dismissal really does follow the collection change immediately (same input/layout frame), then 1000 ms is orders of magnitude more than the correlation needs, and the surplus is exactly long enough for the user to do something unrelated. The mis-attribution case is a removal that does not itself start a dismiss transition — a background window of the hovered group closing, or a thumbnail closed while the pointer happens to stay over a neighbouring one. The tag stays armed, and the next genuine pointer-exit close within one second is then charged the removal delay: 500 ms instead of the 200 ms closeDelayMs (or instead of instant, for users who set closeDelayMs to 1 — which is what the mod is named after).

Suggest tightening THUMBNAIL_REMOVAL_TAG_LIFETIME_MS to something on the order of 100–200 ms: still comfortably covers removal → layout shrink → pointer-exit → dismiss-pending, but a deliberate pointer move no longer lands inside it. Don't go far below ~50 ms, since GetTickCount64 has ~15.6 ms granularity. Alternatively, if the transition is issued inside the OnSourceArrayChanged_Original call frame on your build, an RAII scope flag around that call would be exact and needs no timeout at all — worth checking, since it would remove the heuristic entirely.

One discriminator that looks tempting but isn't viable: clearing the tag when the pointer leaves the flyout frame (SetIsPointerOverFlyoutFrame(false)). That is precisely what fires in the case this feature targets — the flyout shrinks out from under the cursor — so it would defeat the feature rather than sharpen it.

Related: the hook tags on any HoverUIItemsCollection instance without correlating it to the active flyout model/controller. All taskbars (Shell_TrayWnd plus every Shell_SecondaryTrayWnd) share one Explorer UI thread, so if more than one collection can be live at a time, a removal in one taskbar's collection can tag the other's dismiss transition. Worth confirming there's only ever one.

2. Default changes: the PR description and the code disagree, and existing users are affected

The PR description says "Tweaked the default close delay to 150ms", but the diff sets closeDelayMs: 200 (and the commit message says 200). Pick one.

More importantly, closeDelayMs changes from 1 to 200 for everyone who never opened the mod's settings page — the default is what Wh_GetIntSetting returns when nothing is stored, so those users silently lose the instant close they installed a mod called "Instant Taskbar Thumbnail Previews" to get. If that's intentional, it's worth saying so explicitly in the changelog so users know to set it back; otherwise, leaving closeDelayMs at 1 and only introducing the new grace delay would keep the update behavior-preserving.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • The setting key no longer matches what the setting does. closeButtonDelayMs dates from a close-button-only design; it now covers every removal cause, and the code calls it g_thumbnailCloseDelayMilliseconds. Renaming it (e.g. thumbnailRemovalDelayMs) is free right now — after merge, a rename silently resets the value for everyone who configured it.

  • README wording collides with an existing setting name. The new paragraph calls it "The thumbnail-close delay", but there is already a setting literally named "Thumbnail close delay" (closeDelayMs), and the new one is named "Delay after closing a thumbnail". Using each setting's $name verbatim in the README would remove the ambiguity.

  • $name undersells the setting. "Delay after closing a thumbnail" reads as preview-initiated closes only, while the README (correctly) says it also applies to windows closing externally. The $description could mention that.

  • The tag is cleared for unrelated objects. g_thumbnailRemovalTick = 0; sits above the g_activeHoverFlyoutController == object guard in ClearActiveHoverFlyoutController, and HoverFlyoutModelDestructor_Hook clears it for any model. With several controllers/models alive on the same thread, an unrelated dismiss or destruction can cancel a pending tag, and the feature quietly falls back to the pointer-exit delay. Moving the clear inside the guard (or dropping it, since the expiry window already handles staleness) would be more predictable.

  • The README still has no screenshot or GIF (also noted on the previous pull request). A short GIF of the previews closing instantly versus with a delay would make the mod page a lot clearer. Only i.imgur.com and raw.githubusercontent.com are allowed image hosts.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Only CollectionChange::ItemRemoved is tagged. If the taskbar ever refreshes the source array wholesale rather than removing entries one by one, the change arrives as CollectionChange::Reset and the grace delay silently doesn't apply. If you've seen that path in practice, treating Reset as a removal (when the collection shrank) would cover it.

  • Closing the last thumbnail of a group. After that removal the collection is empty, so there is no remaining preview for the pointer to reach — but the dismiss transition is still tagged and the flyout would linger for the full grace delay. Worth checking on a live build whether that path goes through TransitionToFlyoutDismissPendingState at all, or whether the flyout is force-hidden instead. If it does linger, skipping the tag when the collection has no items left (via sender's size) would avoid an empty flyout hanging around for 500 ms.

  • The removal delay replaces the pointer-exit delay rather than extending it. Setting closeButtonDelayMs below closeDelayMs therefore makes closing a thumbnail dismiss the flyout faster than simply moving the pointer away. That's a defensible reading of "exact delay", just noting it's a configuration users can end up with by accident.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 2, 2026
Keep remaining previews open for a configurable delay whenever a thumbnail is removed, including close-button, supported middle-click, keyboard, and external window closes. Correlate removal events with the active flyout within a 150 ms window without adding timers.

Default the general thumbnail close delay to 200 ms and the post-removal delay to 500 ms.
@alchemyyy
alchemyyy force-pushed the instant-taskbar-thumbnail-previews branch from 5b4a3b6 to 3eab9a1 Compare August 2, 2026 05:48
@alchemyyy

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 2, 2026
@windhawk-reviewer

Copy link
Copy Markdown

This pull request has already had 2 AI reviews in the last 24 hours, which is the limit, so no review was posted this time.

Comment /ai-review again after 2026-08-03 01:17 UTC (in 19 hours) to get another one.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 2, 2026
@alchemyyy

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 3, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The new "delay after thumbnail removal" feature itself is a reasonable design — reusing the existing MouseHoverTime override with a second value, keyed off OnSourceArrayChanged(ItemRemoved), is a sensible way to get there without timers. Most of the findings below are about the surrounding infrastructure rewrite, which grew a lot in this PR (+1158/-316) and replaced several things that were already working.

1. The README image is broken and uses a host that isn't allowed.

![https://github.com/user-attachments/assets/ac176503-f162-4da6-8f4d-b6a8dac5cd79]

Two problems: this isn't valid image markdown (![alt](url) — the URL has to be in parentheses), so nothing renders; and github.com/user-attachments isn't an accepted image host. Only i.imgur.com and raw.githubusercontent.com are allowed (the image is mirrored to mods.windhawk.net after merge). Re-upload the demo and use e.g.:

![Demo](https://i.imgur.com/xxxxxxx.gif)

For reference, taskbar-dock-animation-plus.wh.cpp uses the raw.githubusercontent.com form and most others use imgur.

2. DispatchMessageW is now hooked process-wide for the entire session, just to run code on the taskbar thread.

v1.0 used the standard wiki RunFromWindowThread (a temporary SetWindowsHookExW(WH_CALLWNDPROC, ...) + SendMessage, torn down immediately). v1.1 replaces it with a permanent global hook on DispatchMessageW plus ~150 lines of ref-counted request/token machinery. Three concerns:

  • DispatchMessageW is one of the hottest functions in explorer.exe, and this hook is on the message loop of every thread in the process (taskbar, each CabinetWClass window, etc.) for the mod's whole lifetime — to support a facility that runs twice per session (settings change and unload).
  • It's less reliable, not more. It only fires if the target thread's loop happens to dispatch via DispatchMessageW; a WH_CALLWNDPROC hook + SendMessage is delivered by the window-message machinery regardless of how (or whether) the receiving thread pumps. If it doesn't fire, Wh_ModBeforeUninit silently blocks TASKBAR_THREAD_DISPATCH_TIMEOUT_MS (2 s) per taskbar window and then skips its cleanup — which means CleanupTaskbarThreadState/DisarmPreviewLightDismiss never runs and the mod's own TaskbarHost::RegisterLightDismiss registration is left behind after the mod is unloaded.
  • The PostMessage + timeout + refcount + token-map design exists only to work around the fact that a posted message can outlive the waiter. SendMessage doesn't have that problem, which is why the wiki pattern is a stack local and ~40 lines.

Recommend restoring the standard implementation — see taskbar-notification-icon-spacing.wh.cpp#L861-L911, which is the pattern used by most taskbar mods (and what this mod shipped in 1.0). That deletes HookDispatchMessageW, DispatchMessageW_Hook, RunFromWindowThreadRequest, the request map, the token counter and TASKBAR_THREAD_DISPATCH_TIMEOUT_MS.

3. Wh_ModAfterInit calls Wh_ApplyHookOperations() on every load, even when there is nothing to apply.

Hooks registered during Wh_ModInit are applied automatically when Wh_ModInit returns. But after Wh_ModInit the mod's own state is queued, so Wh_ModAfterInitApplyPendingTaskbarHooks()HookStateNeedsApplication(queued, …) is true → Wh_ApplyHookOperations() runs again on the normal path. The API is explicitly documented as expensive:

Note: This function is very slow, avoid using it if possible. Ideally, all hooks should be set in Wh_ModInit and this function should never be used.

That cost is paid on every Explorer start / mod reload for no benefit. Hooks registered in Wh_ModInit should be marked applied by the time Wh_ModAfterInit runs, and Wh_ApplyHookOperations() reserved for the late-load path (LoadLibraryExW_Hook), which is the only place it's actually needed. The comment above the call ("Windhawk attempts to apply hooks before this callback, but doesn't expose that result") isn't a reason to redo it — if the hooks failed to apply, calling it again with nothing pending won't fix that.

4. Failed symbol resolution is retried up to three times against the same module.

MAXIMUM_HOOK_INSTALL_ATTEMPTS = 3 plus g_taskbar*HookResolutionAttempts means that after a HookSymbols failure, the next LoadLibraryExW in the process re-enters ResolveTaskbarSymbols/ResolveTaskbarViewSymbols and calls HookSymbols on the same module again. HookSymbols shouldn't be called more than once for a given module: the resolved symbols are cached, and each additional call invalidates that cache and forces a full re-resolution, which is slow (potentially a symbol download). It can also re-register hooks for the symbols that did resolve on the first pass. And the retry can't help anyway — a symbol either exists in that build or it doesn't, so the second and third attempts fail identically while costing the user a visible stall. Please resolve each module exactly once and drop the attempt counters and the applyFailed/retry states.

Related: when the target modules never load (e.g. the mod is enabled on a Windows 10 Explorer, where neither Taskbar.dll nor Taskbar.View.dll/ExplorerExtensions.dll exists), g_taskbarHookInstallState stays notStarted, so HookInstallationFinished is never true, so g_hookInstallationPending stays true for the life of the process. Every LoadLibraryExW on every Explorer thread then takes the exclusive g_hookInstallLock and re-runs the module lookups forever. A terminal "not applicable on this build" state would settle it.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • TaskbarModelHostWindowID_Hook has a reentrancy hole: it calls TaskbarModelIsExpanded_Original(object) before setting capture->captured = true, so if IsExpanded ever ended up calling HostWindowId on the same model, the hook would recurse without bound. Setting the flag first (and filling the fields after) closes it for free:

    capture->captured = true;   // claim the slot before reentering taskbar code
    try {
        capture->isExpanded = TaskbarModelIsExpanded_Original(object);
        capture->hostWindowID = hostWindowID;
    } catch (...) {
        capture->captured = false;
    }
  • HookInstallState::resolving is unreachable. The only way to re-enter resolution is through HandleLoadedModule, which already bails out on the g_hookInstallationInProgress thread-local, and everything else runs under g_hookInstallLock. Dead state in a 6-state enum.

  • Wh_ModSettingsChanged computes loadedTaskbarModule and passes it to HandleLoadedModule, which ignores the argument except for a null check. Either use it or drop the parameter.

  • In TransitionToFlyoutVisiblePendingState_Hook, ClearThumbnailRemovalTag() runs unconditionally before the CanUseThumbnailRemoval() check; and in HoverUIItemsCollectionOnSourceArrayChanged_Hook, eventArguments.CollectionChange() (an ABI call) is evaluated before the same check. Both are harmless, but moving them inside the guard keeps the "feature unavailable → do nothing" property obvious.

  • The PR description says "Tweaked the default close delay to 150ms", but the setting default is 200 (150 is THUMBNAIL_REMOVAL_CORRELATION_WINDOW_MS, an internal constant). Worth fixing the changelog so it matches the README and the settings block.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Changing the closeDelayMs default from 1 to 200 changes behavior for every existing user who never touched that setting — on a mod called "Instant Taskbar Thumbnail Previews". It's documented in the README, which helps, but it's worth double-checking that's the intent versus leaving the default at 1 and mentioning 200 as a suggested value.

  • THUMBNAIL_REMOVAL_CORRELATION_WINDOW_MS = 150 decides whether the whole feature applies, and it isn't configurable. If the dismiss transition arrives later than that (busy machine, slow XAML layout pass), the feature silently degrades to closeDelayMs with no indication. Since the correlation window is the fragile part of the design, exposing it as a setting — or logging when a removal tag expires unconsumed — would make it debuggable for users who report "it sometimes doesn't work".

  • TrackActiveThumbnailItemsCollection records whichever HoverUIItemsCollection most recently got SetTargetItem while a flyout model is active, and assumes it's the one owned by the visible flyout. If more than one collection is alive (e.g. during a hover hand-off between two taskbar buttons), a removal could be attributed to the wrong flyout, or missed. It fails closed, so worst case is just the normal close delay, but it's a heuristic worth knowing about.

  • ThumbnailRemovalLeavesPreviewItems requires at least one remaining item, so closing the last thumbnail deliberately uses closeDelayMs rather than thumbnailRemovalDelayMs. That's the right call, but the $description for "Delay after thumbnail removal" doesn't say so — a user closing a single-window group will see the short delay and may think the setting is broken.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 3, 2026
@alchemyyy
alchemyyy force-pushed the instant-taskbar-thumbnail-previews branch from 3b9f10e to 9002a6b Compare August 3, 2026 06:22
@alchemyyy

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 3, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


Two things to fix — a broken README image and a lock that can hang Explorer.

1. The README image is broken and uses a disallowed host (line 18)

![https://github.com/user-attachments/assets/ac176503-f162-4da6-8f4d-b6a8dac5cd79]

Two separate problems:

  • That isn't valid markdown image syntax. ![...] with no (...) is a shortcut reference link, so with no matching link definition it renders as the literal text ![https://…] — no image is shown anywhere.
  • github.com/user-attachments is not an allowed image host. Only i.imgur.com and raw.githubusercontent.com may be used (Windhawk mirrors the image after merge).

Re-upload to imgur, or commit the file to your own GitHub repo and link it via raw.githubusercontent.com, and use proper syntax:

![Demo](https://i.imgur.com/xxxxxxx.gif)

Also note that if the asset is an .mp4, it can't be embedded — export it as a GIF. Adding a visual here is a good idea, it just needs to actually render.

2. g_hookInstallLock is a lock-order inversion against the loader lock

The new hook-install state machine takes g_hookInstallLock (line 104) from two kinds of call sites:

  • From inside LoadLibraryExW_HookHandleLoadedModule (line 1758) — this runs under the loader lock.
  • From Wh_ModInit (line 1981), Wh_ModAfterInit (line 1995) and Wh_ModSettingsChanged (via HandleLoadedModule) — where the lock is held across ResolveTaskbarSymbols/ResolveTaskbarViewSymbols, i.e. across WindhawkUtils::HookSymbols and Wh_ApplyHookOperations. With a cold symbol cache that can take seconds and can itself load modules.

That's the classic inversion: thread A holds g_hookInstallLock and ends up needing the loader lock (inside symbol resolution), while an Explorer thread loading Taskbar.View.dll holds the loader lock and blocks on g_hookInstallLock — Explorer hangs. Wh_ModAfterInit is the most likely window, because it publishes g_lateHookApplicationReady = true and then resolves symbols, all while still holding the lock (lines 2010–2018) — and "Taskbar.View.dll loads shortly after init" is exactly the scenario this code exists to handle.

The lock isn't buying anything a one-shot atomic can't. Existing mods resolve each module exactly once with a plain std::atomic<bool>::exchange and no lock at all — see taskbar-clock-customization.wh.cpp#L5319-L5343:

void HandleLoadedModuleIfSystemTray(HMODULE module, LPCWSTR lpLibFileName) {
    if (g_winVersion >= WinVersion::Win11 && !g_systemTrayModuleHooked &&
        GetSystemTrayModuleHandle() == module &&
        !g_systemTrayModuleHooked.exchange(true)) {
        if (HookSystemTraySymbols(module)) {
            Wh_ApplyHookOperations();
        }
    }
}

Version 1.0 of this mod used the same compare_exchange_strong shape and didn't need a lock. If you want to keep the richer waitingForModule / queued / applied / failed states for the Can* predicates, they can live in a std::atomic<HookInstallState> each — the one-shot transition out of waitingForModule is the only thing that needs to be atomic, and dropping the lock also lets you delete HookInstallationScope / g_hookInstallationInProgress (their only job is to keep a reentrant LoadLibraryExW from self-deadlocking on that same non-recursive lock).

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • Redundant gating inside the hooks. CanTrackActiveHoverFlyout() / CanTrackFlyoutPointerState() / CanUseThumbnailRemoval() etc. each re-check g_taskbarViewHooksApplied plus 6–10 function-pointer loads, and they're called from SetIsPointerOverFlyoutFrame_Hook, HoverUIItemsCollectionSetTargetItem_Hook and friends. A Taskbar.View hook can only ever run because those hooks were applied and those symbols resolved, so the checks are tautological inside them. Collapsing each predicate into a single bool/std::atomic<bool> computed once right after Wh_ApplyHookOperations would be both cheaper and easier to read.

  • MINIMUM_REMAINING_THUMBNAIL_COUNT (line 74) is a named constant used exactly once, as >= 1. ThumbnailSourceArraySize_Original(sourceArray) > 0 says the same thing more directly.

  • User-facing text leaks implementation detail. The $description for thumbnailRemovalDelayMs ("Delay used when dismissal is attributed to a recent thumbnail removal … Attribution uses a short event-correlation window") and the matching README paragraph describe the mod's internals rather than what the user gets. Something like "How long the remaining previews stay open after one of them is closed" would read better; the correlation-window mechanics can stay in a code comment.

  • TaskbarHostDestructor_Hook (line 575) calls _Original first and then cleans up, while the other three destructor hooks clean up first and then call _Original. Both are correct here (only the pointer value is used as a key), but matching the order would remove a "why is this one different?" question for the next reader.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • The 150 ms correlation window cuts both ways. THUMBNAIL_REMOVAL_CORRELATION_WINDOW_MS (line 82) is fairly tight for a XAML app: if the dismiss transition arrives later than that (busy machine, a slow frame, a deferred layout pass), the removal delay silently doesn't apply and the user gets the 200 ms close delay instead. In the other direction, a genuine pointer-exit that happens within 150 ms of closing a thumbnail gets the 500 ms delay it wasn't meant to get. You already log the expiry case, which helps. Worth considering whether the window can be looser — removalFlyoutModel is already invalidated by the next TransitionToFlyoutVisiblePendingState, by ~HoverFlyoutModel, and by ~HoverUIItemsCollection, so the timer is mostly a belt-and-braces guard against a much later unrelated pointer exit rather than the primary correlation mechanism.

  • The collection↔model association is purely temporal. activeThumbnailItemsCollection is whatever object gets SetTargetItem after a visible-pending transition (TrackActiveThumbnailItemsCollection, line 965). If that ordering ever changes on a future build, OnSourceArrayChanged's object == activeThumbnailItemsCollection check just never matches and the whole feature quietly stops working, with no log line to explain it — unlike the expiry path. A Wh_Log when a removal is observed on an untracked collection would make that failure mode diagnosable from a user's log.

  • The closeDelayMs default change from 1 to 200 applies to every existing user who never touched that setting, so the mod's behavior changes on update. You do call this out in the README, which is the right thing to do — just noting that a mod named "Instant Taskbar Thumbnail Previews" now defaults to a non-instant close is a slightly odd fit. Not a blocker, it's your product call.

  • Overlap with your own taskbar-thumbnail-hover-fix. That mod reacts to the same event this PR now detects — a thumbnail's window closing while the flyout is open — but via a process-wide EVENT_OBJECT_DESTROY SetWinEventHook plus a synthetic SendInput mouse jiggle, which is a much blunter instrument. Now that this mod has precise in-process detection (HoverUIItemsCollection::OnSourceArrayChanged), the hover-refresh behavior could become an option here and drive off the same signal, letting the other mod be retired. The maintainer generally prefers one richer mod over several overlapping ones, so it may be worth considering.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-for-author The author's turn: request an AI review, or respond to one that was posted.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant