Skip to content

Add Taskbar Horizontal Scroll Media Control v1.0.0 - #5023

Open
shreyasjswork wants to merge 1 commit into
ramensoftware:mainfrom
shreyasjswork:agent/add-taskbar-horizontal-scroll-media
Open

Add Taskbar Horizontal Scroll Media Control v1.0.0#5023
shreyasjswork wants to merge 1 commit into
ramensoftware:mainfrom
shreyasjswork:agent/add-taskbar-horizontal-scroll-media

Conversation

@shreyasjswork

Copy link
Copy Markdown

Adds Taskbar Horizontal Scroll Media Control v1.0.0.

The mod provides next/previous track control through horizontal mouse-wheel tilt over the taskbar, optional modifier handling, Ctrl + middle-click play/pause on empty taskbar space, continuous-input suppression, and Windows' genuine Quick Settings media card as a standalone flyout when the panel is closed.

Validation:

  • Official pr_validation.py check passed for shreyasjswork.
  • The pull request changes only mods/taskbar-horizontal-scroll-media.wh.cpp.
  • The release source compiles successfully with the Windhawk compiler.

Changelog

Not applicable — this pull request introduces a new mod.

Mod authorship

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):

@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Aug 5, 2026
@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.

@shreyasjswork
shreyasjswork marked this pull request as ready for review August 5, 2026 12:55
@shreyasjswork
shreyasjswork force-pushed the agent/add-taskbar-horizontal-scroll-media branch from ea819d0 to 2b89c97 Compare August 5, 2026 13:02
@shreyasjswork

Copy link
Copy Markdown
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 5, 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 gesture handling is solid, but the ShellHost half — capturing Windows' live ControlCenter.MediaTransportControls and reparenting it into a XAML island — has a number of lifetime and shutdown problems, and there is a lot of leftover debug instrumentation. The main items:

1. Debug diagnostics are written to persistent mod storage. There are 25 Wh_SetIntValue calls (SetDiagnostic, SetRouterDiagnostic, nativeCardTopDip, playPauseDiagHresult, panelDiagProcessId, …) against 5 Wh_Log calls. Wh_SetIntValue writes to the mod's persistent local storage, so this is a synchronous store write on the XAML UI thread, inside an EnumWindows callback, and on every gesture — and it leaves permanent junk behind. Windhawk already provides exactly this facility: Wh_Log compiles to a cheap disabled-by-default check and prefixes the mod name. Please replace all of the diagnostic Wh_SetIntValue/SetDiagnostic/SetRouterDiagnostic machinery with Wh_Log.

2. Windhawk.TaskbarHorizontalScrollMedia.CapturedNativeHost is registered but never unregistered. EnsureCapturedHostWindow calls RegisterClassExW and continues on ERROR_CLASS_ALREADY_EXISTS, and there is no UnregisterClass in Wh_ModUninit. A window class is not removed when the mod DLL unloads, so after a mod toggle/update the registration survives with lpfnWndProc pointing into the old (unmapped or relocated) image. Either CreateWindowExW then fails because the new hInstance doesn't match the stale registration and the flyout silently stops working, or — if the image happens to land at the same base — messages are dispatched to a dangling WndProc. The ERROR_CLASS_ALREADY_EXISTS fallthrough is the workaround, not the fix; remove it and unregister on unload instead:

// In the teardown that runs on the XAML UI thread, after DestroyWindow:
UnregisterClassW(kCapturedHostClass, instance);  // mod's own module handle

See autoscroll-win32.wh.cpp#L1051 for the pattern.

3. Global WinRT/XAML objects: missing [[clang::no_destroy]], and one release happens on the wrong thread. g_capturedMedia (a strong FrameworkElement), g_originalMediaParent (Panel), g_capturedDispatcher, g_capturedXamlSource and g_visualTreeWatcher are globals with non-trivial destructors. Wh_ModUninit does not run when ShellHost.exe itself terminates (sign-out, shell restart) — but the CRT still runs these destructors, alone on the shutdown thread after every other thread has been killed, with the XAML core already gone. On top of that, ShutdownMediaCapture does g_capturedMedia = nullptr; / g_capturedDispatcher = nullptr; on the arbitrary Wh_ModUninit thread, releasing a thread-affine XAML reference off the UI thread, even on the normal unload path. Both are covered by Global objects and process shutdown — see #4-xaml-and-other-ui-thread-objects. These types are all nullable/handle-like, so the bare attribute is the right form:

[[clang::no_destroy]] winrt::Windows::UI::Xaml::FrameworkElement g_capturedMedia{nullptr};
[[clang::no_destroy]] winrt::Windows::UI::Xaml::Controls::Panel g_originalMediaParent{nullptr};
[[clang::no_destroy]] winrt::Windows::UI::Core::CoreDispatcher g_capturedDispatcher{nullptr};
[[clang::no_destroy]] winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource g_capturedXamlSource{nullptr};
[[clang::no_destroy]] winrt::com_ptr<MediaVisualTreeWatcher> g_visualTreeWatcher;

and the explicit release must stay, but move g_capturedMedia = nullptr (and g_originalMediaParent) inside the RunAsync lambda that already runs on the captured dispatcher. taskbar-folder-menus.wh.cpp#L737 shows the same shape.

Related: g_capturedMedia / g_capturedDispatcher are non-atomic WinRT smart pointers written from the XAML thread in OnVisualTreeChange and read from the native-host thread in RequestCapturedStandalone and from the unload thread in ShutdownMediaCapture, with only g_mediaCaptured (atomic) as a barrier. That's a genuine data race on the com_ptr; guard the handoff with a mutex, or keep everything that touches these objects on the dispatcher thread.

4. Unload waits are bounded, then the DLL is unloaded regardless. StopRuntime does WaitForSingleObject(thread, 5000) for each thread and proceeds on timeout, and ShutdownMediaCapture does WaitForSingleObject(completed, 3000) and then CloseHandle(completed). Both timeouts are reachable:

  • RouterThreadProc can be sitting in ShellExecuteW(L"ms-controlcenter:"), in a UIA ElementFromPoint against a busy shell, or in ToggleCurrentMediaSession()'s two blocking .get() calls — none of which are bounded by 5 s.
  • If the dispatcher lambda hasn't run within 3 s, the handle is closed under it and the lambda later calls SetEvent on a closed handle and executes code in a DLL that is about to be unloaded.

Once Wh_ModUninit returns, the image is unmapped, so a thread that is still running returns into unmapped memory. Make these waits unbounded (INFINITE) once the stop signal is set, and make the stop signal actually interrupt the blocking calls — e.g. don't call ShellExecuteW / the blocking .get()s without first re-checking g_stopEvent, and give the completion event a lifetime that outlives the lambda (or keep waiting until it is signalled).

5. The mod initializes in every explorer.exe, not just the shell instance. DetectProcessRole only checks the image name, so with "Launch folder windows in a separate process" enabled (or during an Explorer restart when instances overlap), each explorer.exe installs its own WH_MOUSE_LL hook and its own router thread. Every one of them sees the same tilt and calls SendInput(VK_MEDIA_NEXT_TRACK), so a single gesture skips two or more tracks. Gate initialization on this process actually owning the shell, e.g.:

DWORD shellPid = 0;
GetWindowThreadProcessId(GetShellWindow(), &shellPid);
if (shellPid != GetCurrentProcessId()) return FALSE;  // not the shell instance

Worth noting more broadly: the explorer.exe half installs no function hooks at all — it only uses SetWindowsHookEx, EnumWindows, UI Automation and SendInput, all of which work from any process. That is the textbook signal for the "mods as tools" pattern: moving that half into a dedicated process would get single-instancing for free and keep a crash in the gesture/UIA code out of the shell. The ShellHost half genuinely needs in-process injection, so this may not be practical for the whole mod — but if you can split it, it's the better architecture.

6. Forcing Quick Settings open and then hiding it. When nothing has been captured yet, RouterThreadProc runs ShellExecuteW(nullptr, L"open", L"ms-controlcenter:", ...), and the bootstrap path in ShowCapturedMediaOnUiThread then does ShowWindow(panelWindow, SW_HIDE) on the real ControlCenterWindow. So the first track change after boot visibly flashes the whole Quick Settings panel open, and hiding a window Windows still considers open leaves the shell's own flyout state inconsistent (the next Win+A may need two presses or land in the wrong state). This is a workaround for "the element doesn't exist yet" rather than a fix — please find a way to obtain the control without driving the user's Quick Settings, or gate the standalone feature off until Windows creates it on its own.

This gets worse on builds where Quick Settings is not in ShellHost.exe. @include only lists ShellHost.exe, but pre-24H2 Windows 11 hosts Quick Settings in ShellExperienceHost.exe (see windows-11-notification-center-styler.wh.cpp#L10-L11, which targets both). There, nothing ever listens on g_showEvent, IsQuickPanelOpen() never finds a ControlCenterWindow, and the fallback fires — so every track change pops the real Quick Settings panel open and nothing ever hides it. Either add ShellExperienceHost.exe support or detect the unsupported case and skip the flyout entirely instead of falling back to ms-controlcenter:.

7. The captured element can go stale or be double-captured. OnVisualTreeChange handles only VisualMutationType::Add and unconditionally overwrites g_capturedMedia / g_capturedDispatcher. ShellHost tears down and recreates ControlCenterWindow and its content, so:

  • if a new MediaTransportControls is added while the previous one is currently reparented into the island, g_capturedMedia is replaced but g_originalMediaParent/g_originalMediaIndex still describe the old element's parent — RestoreCapturedMedia then inserts the new element into the old parent, and the old element is orphaned inside the island forever;
  • Remove mutations are ignored, so g_capturedMedia can keep pointing at an element Windows has already discarded.

Handle VisualMutationType::Remove (clear the capture when the captured handle is removed), and ignore/restore-first when an Add arrives while g_originalMediaParent is set.

8. The dismiss timer is never stopped. SetTimer(g_capturedHostWindow, kDismissTimer, 100, nullptr) is started when the card is shown, but RestoreCapturedMedia only hides the window — KillTimer happens solely in WM_DESTROY. So after the first standalone display, a 100 ms timer wakes the Quick Settings UI thread ten times a second for the rest of the ShellHost process lifetime. Add KillTimer(g_capturedHostWindow, kDismissTimer); to RestoreCapturedMedia.

9. The shared events are writable by every process in the session, including untrusted ones. CreateSharedEvent uses D:P(A;;0x00100002;;;WD)…S:(ML;;NW;;;S-1-16-0)EVENT_MODIFY_STATE | SYNCHRONIZE granted to Everyone plus the AppContainer SIDs, with an Untrusted integrity label. Any sandboxed process (a browser renderer, for instance) can signal …ShowNativeControl.v4 and make the shell display the flyout or run ShellExecuteW(ms-controlcenter:). Explorer and ShellHost both run as the interactive user at medium integrity, so none of that is needed — a default DACL (CreateEventW(nullptr, …) with a name) is sufficient; if you do need an explicit descriptor, grant the current user only.

10. License compatibility. The XAML-diagnostics TAP scaffolding (MediaVisualTreeWatcher with the AdviseVisualTreeChange-on-a-new-thread trick, SimpleClassFactory, DllGetClassObject/DllCanUnloadNow, the FreeLibrary in SetSite to undo InitializeXamlDiagnosticsEx's refcount, the VisualDiagConnection%d 1..10000 loop) is structurally identical to windows-11-taskbar-styler.wh.cpp, which is published under GPLv3. If it is derived from there, @license MIT isn't compatible — please either confirm it's an independent implementation or switch the license.

11. Ctrl+middle-click ignores the modifier setting. LowLevelMouseProc calls IsCtrlPressed() for WM_MBUTTONDOWN, which is hardcoded to Ctrl, while the wheel gesture honours requireModifier/modifier. A user who selects Shift gets Shift for tilt but still Ctrl for play/pause, and requireModifier: false has no effect on middle-click at all. Either use IsModifierPressed() for both, or document that the play/pause chord is fixed.

12. Overlap with an existing mod. The track-switching half is close to Taskbar Scroll Actions, which already implements "assign an action to scrolling over the taskbar" with configurable scroll areas, step and throttling. Adding a next/previous track action plus horizontal-wheel support there would cover the gesture for everyone using that mod, and would leave this mod free to be just the standalone-Quick-Settings-card feature (which is genuinely new). Please state in the README how this differs from that mod, and consider proposing the gesture upstream instead.

13. Add a screenshot or GIF to the README. The mod's headline feature is a visible flyout hosting Windows' own media card; a screenshot (or a short GIF of the tilt gesture) makes it much easier to judge from the catalog. Only i.imgur.com and raw.githubusercontent.com are allowed image hosts.

Optional improvements

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

  • LoadSettings uses a raw Wh_GetStringSetting + Wh_FreeStringSetting; WindhawkUtils::StringSetting::Make(L"modifier") is the RAII form and is preferred. Also, Wh_GetStringSetting never returns NULL — it returns L"" on error/unset — so the if (value) guard is dead code.
  • Unused dependencies: #include <vector> (no std::vector in the file), <windowsx.h> and <activation.h> appear unused, and -lgdi32 isn't needed (no GDI calls). Conversely, uint32_t/uint64_t are used without #include <cstdint>.
  • IsPointOverTaskbar walks parents manually and then calls GetAncestor(window, GA_ROOT). The GetAncestor call alone covers the case; the loop is redundant.
  • ProcessRole::Unsupported can't happen given the @include list — DetectProcessRole and the whole CurrentProcessName helper are effectively dead once the shell-instance check from item 5 exists.
  • SetDiagnostic writes both nativeDiagCheckpoint and nativeDiagStage for the same value; if any of this survives as logging, the duplication isn't needed.
  • ClampSetting silently clamps user input (e.g. releaseTimeoutMs to 80–2000, nativeFlyoutTimeoutMs to 1500–15000) without the settings $description mentioning the accepted range — worth documenting so a user who types 5000 isn't confused.
  • wsprintfW is a legacy API with no bounds checking; swprintf_s (or std::format, since this is C++23) is a better fit for VisualDiagConnection%d.
  • Wh_ModInit blocks up to 5 s in InitializeExplorerRole waiting for g_hookReadyEvent. It normally completes instantly, but Wh_ModInit runs before the process starts executing — a shorter timeout would bound the worst case on shell startup.

Functionality notes

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

  • Reparenting a live element out of Windows' own visual tree is inherently fragile: any path where RestoreCapturedMedia doesn't run (an exception during restore, a hard ShellHost crash, the stale-capture case in item 7) leaves Quick Settings without its media card until the process restarts. Worth calling out in the README so users know what a failure looks like, and worth being conservative about when the capture is attempted at all.
  • MeasureNativeCardBounds picks the card by scoring "widest Windows.UI.Xaml.Controls.Border that fits", with hardcoded fallbacks (448×224, accepted ranges 320–700 and 100–500 DIPs) and a 4 DIP bottom slack. This will need re-tuning whenever the Quick Settings template changes. If the control exposes a named part you could reach via FindName/VisualTreeHelper by name, that would be considerably more stable.
  • Flyout placement assumes a bottom taskbar — GetBottomTaskbarTop explicitly looks for a taskbar in the lower half of the monitor and otherwise falls back to rcWork.bottom. Fine for stock Windows 11, but the card will sit in the wrong place for users who moved the taskbar with another mod.
  • The card is only removed when the 100 ms timer next fires, so opening Quick Settings while the standalone card is visible can briefly show the panel without its media card. Reacting to the panel opening rather than polling would remove the gap.
  • On the input side, taskbar-scroll-actions intercepts taskbar wheel input by hooking the InputSite window proc (WM_POINTERWHEEL) rather than installing a system-wide WH_MOUSE_LL hook. If the Windows 11 taskbar delivers a horizontal equivalent there, that would avoid putting a global input hook in the chain and remove the WindowFromPoint heuristics entirely. If it doesn't, the low-level hook is a reasonable choice — just worth checking.


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 5, 2026
@shreyasjswork
shreyasjswork force-pushed the agent/add-taskbar-horizontal-scroll-media branch from 2b89c97 to f3c6b04 Compare August 5, 2026 13:27
@shreyasjswork

shreyasjswork commented Aug 5, 2026

Copy link
Copy Markdown
Author

Addressed the review against the updated v1.0.0 commit:

  • Replaced persistent diagnostic storage writes with Wh_Log.
  • Added deterministic host-window-class teardown.
  • Marked global XAML/WinRT holders no_destroy, synchronized the cross-thread handoff, and moved releases to the captured UI dispatcher.
  • Made unload waits safe and unbounded after signalling stop; queued UI cleanup now owns its completion event until execution.
  • Limited input-hook initialization to the shell Explorer instance.
  • Added build-gated ShellExperienceHost.exe support for pre-24H2 Windows without injecting both Quick Settings hosts.
  • Handled removed/recreated media controls and guarded intentional reparent mutations.
  • Stopped the dismiss timer whenever the standalone card is restored.
  • Replaced the Everyone/AppContainer event ACL with the interactive user's default DACL.
  • Changed the license to GPL-3.0 because the XAML diagnostics scaffold derives from the GPL reference implementation.
  • Clarified that modifier settings affect wheel tilt only; Ctrl + middle-click remains the requested fixed play/pause chord.
  • Documented the Taskbar Scroll Actions distinction, accepted setting ranges, older host support, and first-use behavior; removed unused includes/libraries and replaced the legacy formatter.

The first-use ms-controlcenter bootstrap is intentionally retained: Windows does not instantiate the genuine ControlCenter.MediaTransportControls until its Quick Settings visual tree exists. The mod documents this, performs it only while no capture exists, and suppresses standalone creation whenever Quick Settings is already open. A screenshot was not added because this source-only PR has no approved external image URL yet.

Validation: zero-warning Windhawk compile, clean official pr_validation.py, one changed mod file, and v1.0.0 remains a single commit.

/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 5, 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 gesture side is nicely built (the hook callback stays cheap, UI Automation is done on a worker thread, and the wheel latching logic is solid), but there are a few blocking problems around process detection, teardown and the Quick Settings bootstrap.

1. The mod never initializes in Explorer after a reboot or an Explorer restart.

Wh_ModInit gates the Explorer role on IsShellExplorerProcess(), which is GetShellWindow() → owning PID:

if (g_role == ProcessRole::Explorer && !IsShellExplorerProcess()) {
    Wh_Log(L"Skipping non-shell Explorer process");
    return FALSE;
}

Wh_ModInit runs before the target process begins its execution (Mod lifetime). When explorer.exe starts as the new shell (boot, sign-in, Explorer restart), Progman doesn't exist yet, so GetShellWindow() returns NULL, the check fails and the mod bails out. It only works if you enable it (or change a setting) while Explorer is already running — which is exactly the "works mid-session but not after a restart" case the maintainer rejects.

The simplest fix is to drop the process-level check and filter at the point where it actually matters — only the shell process owns Shell_TrayWnd:

bool IsTaskbarClass(HWND window) {
    DWORD processId = 0;
    GetWindowThreadProcessId(window, &processId);
    if (processId != GetCurrentProcessId()) return false;
    ...
}

That also makes the mod inert in short-lived / separate-process Explorer instances without needing GetShellWindow() at all.

2. The host window and its window class survive mod unload → next load's flyout is dead, and ShellHost can crash.

ShutdownMediaCapture only destroys g_capturedHostWindow and calls UnregisterCapturedHostClass() from inside the dispatcher callback, and that whole block is skipped when there's no dispatcher:

{
    std::lock_guard lock(g_captureMutex);
    if (g_capturedDispatcher) dispatcher = g_capturedDispatcher;
}
if (dispatcher) { /* ...DestroyWindow + UnregisterCapturedHostClass... */ }

But ClearCapturedMediaOnUiThread() sets g_capturedDispatcher = nullptr every time Windows removes the captured element from the visual tree (i.e. whenever the Control Center view is torn down), while RestoreCapturedMedia() only hides the host window. So the very common sequence "show the card → restore → Windows destroys the Control Center tree → unload the mod" leaves:

  • a live top-level Windhawk.TaskbarHorizontalScrollMedia.CapturedNativeHost window whose lpfnWndProc points into the unmapped mod image — any broadcast message (WM_SETTINGCHANGE, WM_THEMECHANGED, WM_DISPLAYCHANGE, …) then crashes ShellHost;
  • the class still registered, so on the next load RegisterClassExW fails with ERROR_CLASS_ALREADY_EXISTS, EnsureCapturedHostWindow() returns false forever, and the standalone card silently stops working until ShellHost restarts.

Fix: keep a dispatcher for the host window's own thread that is not cleared when the captured element goes away (or simply don't null g_capturedDispatcher in ClearCapturedMediaOnUiThread — only the element handle needs clearing), and always destroy the window + unregister the class on unload. Note the window must be destroyed on the thread that created it, so if that thread is gone you still need a path that unregisters the class. See taskbar-shadow-border.wh.cpp#L472 for the register-on-load / unregister-on-unload shape.

3. Unload can hang ShellHost indefinitely.

dispatcher.RunAsync(CoreDispatcherPriority::Low, [completed] { ... });
WaitForSingleObject(completed, INFINITE);

Wh_ModUninit runs on the Windhawk engine thread. If the Control Center UI thread has stopped pumping (it's being torn down, or it's blocked) after RunAsync succeeded, this waits forever and the mod never unloads — Windhawk's unload/update hangs and the process is left in a broken state. Use a bounded wait (a few seconds) and log on timeout, and make the callback tolerate running after the wait gave up.

4. EnumWindows isn't filtered by process before hiding a window.

BOOL CALLBACK FindControlCenterWindowProc(HWND window, LPARAM parameter) {
    wchar_t className[128];
    GetClassNameW(window, className, ARRAYSIZE(className));
    if (wcscmp(className, L"ControlCenterWindow") == 0) { ... }

…followed by ShowWindow(panelWindow, SW_HIDE). EnumWindows enumerates every top-level window on the desktop, so this can hide a window belonging to another process. Add a GetWindowThreadProcessId(window, &pid) == GetCurrentProcessId() check. (IsQuickPanelOpen is deliberately cross-process, so that one is fine as is — but it should still not act on foreign windows.)

Also, both callbacks wcscmp on className without checking the GetClassNameW return value; on failure the buffer is uninitialized stack memory.

5. When the ShellHost half isn't available, every gesture pops open Quick Settings and leaves it open.

The router falls back to ShellExecuteW(nullptr, L"open", L"ms-controlcenter:", ...) whenever g_captureReadyEvent isn't signaled. The panel is only hidden again from the ShellHost side, in the bootstrap branch of ShowCapturedMediaOnUiThread. So on any system where the ShellHost half never captures anything — Windows 10 (DetectProcessRole happily returns ShellHost for ShellExperienceHost.exe on build 19045, where ControlCenter.MediaTransportControls doesn't exist), ShellHost excluded from injection, or the TAP failing — the user gets Quick Settings opening on every wheel tilt and staying open. That's a very unpleasant failure mode for what should just be "skip track".

Please (a) gate the bootstrap so it is attempted at most once per ShellHost lifetime and never when the ShellHost half hasn't announced itself (a separate manual-reset "alive" event signalled from InitializeShellRole would do), and (b) return ProcessRole::Unsupported for builds below Windows 11 (build < 22000) and state the minimum supported build in the README.

Related: InjectMediaCaptureTAP() is called directly from Wh_ModInit/InitializeShellRole, which at ShellHost process start runs before XAML exists, so that first injection essentially always fails. The established pattern is to inject once a XAML window exists — see windows-11-notification-center-styler.wh.cpp#L9661, which hooks window creation and injects when ControlCenterWindow / Windows.UI.Core.CoreWindow appears (with an explicit "initializing at this point is too early and doesn't work" note). Doing the same here would let the watcher be advised before the first gesture and avoid the visible Quick Settings flash.

6. g_captureReadyEvent is left signaled when the ShellHost half unloads.

StopRuntime closes the handle but never resets it, and the named event object stays alive as long as Explorer holds its handle. After unloading/reloading the mod in ShellHost, Explorer still sees "capture ready", skips the bootstrap, and the card never appears. Add a ResetEvent(g_captureReadyEvent) in the ShellHost teardown path before closing.

7. Don't call SendInput from inside the WH_MOUSE_LL callback.

SendTrackMediaKeySendMediaVirtualKeySendInput runs synchronously inside LowLevelMouseProc, which blocks system-wide input processing and re-enters the input stack from a hook callback. The mod already has the router thread for exactly this (play/pause is dispatched there), so the same treatment applies — set a dedicated event with the direction and do the work on the router thread. Everything else in the hook callback is already appropriately cheap, so this is the only piece that needs moving.

8. 100 ms polling on a shell UI thread.

SetTimer(g_capturedHostWindow, kDismissTimer, 100, nullptr);

Every tick runs IsQuickPanelOpen(), which is a full EnumWindows over every top-level window on the desktop, doing IsWindowVisible + IsIconic + DwmGetWindowAttribute (a DWM RPC) + GetWindowRect + GetClassNameW on each — 10×/second, on the Control Center XAML UI thread, for up to 15 seconds. Constant polling like this is a recurring objection. At minimum, use a one-shot SetTimer for the dismissal deadline instead of polling for it, and compare the class name first in FindOpenPanelProc so the DWM/rect calls only run for ControlCenterWindow. Better still, detect the panel opening with SetWinEventHook(EVENT_OBJECT_SHOW) instead of polling.

9. Process model — please justify keeping the gesture inside explorer.exe.

The Explorer role installs no function hooks at all; it only uses SetWindowsHookEx, EnumWindows, WindowFromPoint, UI Automation, SendInput and named events — all of which work from any process. That's the textbook shape for the Mods as tools pattern, and the reason it's preferred is that a crash or a stall in the mod currently takes the shell with it. I realize the ShellHost half genuinely needs in-process XAML access, so a single tool mod isn't possible — but please either move the gesture side into the ShellHost half (which would drop the whole cross-process event channel and the shell-detection problem in item 1), or explain in the PR why it has to live in explorer.exe (e.g. ShellExperienceHost.exe runs in an AppContainer and can't install a global hook). Right now it reads as an unexamined default.

10. Overlap with Taskbar Scroll Actions.

The README pre-empts this, and the standalone media card is genuinely new — but "wheel over the taskbar performs an action" is already owned by Taskbar Scroll Actions, which has a general action/area/step/throttle model. The maintainer's consistent preference is to extend an existing mod rather than add a variation. Adding a "next/previous track" action plus horizontal-wheel support there would cover the gesture half with settings users already know. Worth considering whether this mod should be just the media-card feature.

11. Add a screenshot or GIF to the README.

The mod's headline feature is a visible card next to the taskbar and there's no image of it. Images must be hosted on i.imgur.com or raw.githubusercontent.com.

Optional improvements

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

  • [[clang::no_destroy]] std::mutex g_captureMutex; — the attribute isn't needed here. std::mutex's destructor is a no-op on Windows, so it's safe at process shutdown; an unnecessary suppression is noise and invites cargo-culting. The attribute is correctly applied to the WinRT/XAML globals below it. See Global objects and process shutdown.
  • Use WindhawkUtils::StringSetting (RAII) instead of Wh_GetStringSetting + manual Wh_FreeStringSetting. Also, Wh_GetStringSetting never returns NULL — it returns L"" — so the if (value) guard in LoadSettings is dead code.
  • LogNativeState(-731, ...) / LogRouterState(450) — the numeric stage codes are opaque to anyone reading the log (including you, in six months). Windhawk already prefixes the mod name and gates logging, so plain Wh_Log(L"...") calls with readable text at each site are both simpler and more useful; custom logging wrappers are discouraged.
  • IsPointOverTaskbar walks parents manually:
    for (HWND current = window; current; current = GetParent(current)) { ... }
    HWND root = GetAncestor(window, GA_ROOT);
    GetParent returns the owner for top-level windows, so the loop can escape into unrelated windows and produce false positives. The GetAncestor(window, GA_ROOT) line below it already does the right thing — drop the loop.
  • g_reparentingCapturedMedia is a plain bool written on the UI thread and read from OnVisualTreeChange; std::atomic<bool> would be more honest (the rest of the shared flags already are).
  • ClampSetting maps any value <= 0 to the default rather than to the documented minimum, which is surprising given the $description says "Accepted range: 80-2000". Clamping to minimum would match the text.
  • Double-check -luiautomationcore — nothing in the mod calls the provider-side Uia* exports; the client interfaces come in via COM and the GUIDs via -luuid.
  • The host window is created on the captured element's dispatcher thread. If Windows ever recreates the Control Center view on a different thread, later SetTimer/ShowWindow/DestroyWindow calls would target a foreign-thread window (DestroyWindow fails outright). Worth asserting the thread or recreating the host when the dispatcher changes.

Functionality notes

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

  • Reparenting Windows' own ControlCenter.MediaTransportControls out of the Quick Settings tree and back is clever but inherently fragile: it depends on a private XAML type name, on FindNativeCardBounds heuristically picking the right Border, and on magic fallbacks (448.0 × 224.0, the 0.85 width ratio, the 4 DIP bottom slack). A Windows update that renames the type or restructures the card will break it silently. There's no obviously better approach for "show the genuine card", so this is an FYI rather than a change request — but the README should say so, and the failure path should leave Quick Settings untouched rather than half-open.
  • Track changes go through SendInput(VK_MEDIA_NEXT_TRACK) while play/pause goes through GlobalSystemMediaTransportControlsSession.TryTogglePlayPauseAsync(). Those can resolve to different apps. TrySkipNextAsync() / TrySkipPreviousAsync() on the same session would make the two gestures consistent (and would fit the "move it off the hook thread" change in item 7 above).
  • Card placement assumes a bottom taskbar: GetBottomTaskbarTop falls back to rcWork.bottom and the card is always pinned bottom-right of the work area. On a top / left / right taskbar (Windows 10, or third-party taskbar positioning), the card won't sit next to the taskbar.
  • IsEmptyTaskbarPoint returns true when the UIA walk runs out of its 20-level budget without ever reaching the taskbar HWND. On a deep or slow-to-resolve tree that turns a click on a real element into "empty space". Returning false on budget exhaustion would be the safer default.
  • The ms-controlcenter: bootstrap means the first gesture after each ShellHost start behaves differently from every later one (Quick Settings is really opened, then hidden). Even when it works, expect a visible flash on some machines — worth mentioning in the README next to the existing paragraph.


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 5, 2026
@shreyasjswork
shreyasjswork force-pushed the agent/add-taskbar-horizontal-scroll-media branch from f3c6b04 to 8f09c57 Compare August 6, 2026 08:16
@shreyasjswork

Copy link
Copy Markdown
Author

Addressed the review against the updated v1.0.0 commit:

  • Removed the startup-time GetShellWindow gate. Every Explorer instance can initialize safely, but gesture hit-testing now accepts only taskbar windows owned by the current process, so separate folder-process instances remain inert.
  • Preserved a separate dispatcher for the native host window, independent of capture removal, and always tears down the XAML island, host window, and registered class on that UI thread during normal unload.
  • Replaced the unbounded dispatcher wait with a five-second bound. The completion handle is shared with the callback, and the module is kept mapped on timeout so a late callback cannot use a closed handle or unmapped code.
  • Restricted the Control Center bootstrap window lookup to the current process and checks both GetClassNameW calls before comparison.
  • Added Windows 11 build 22000 gating, ShellHost alive state, and a single-consumption bootstrap token. Quick Settings can be opened for capture at most once per host lifetime and never when the owning host has not announced itself.
  • Moved XAML diagnostics injection out of early Wh_ModInit: a process-filtered EVENT_OBJECT_SHOW hook requests injection when ControlCenterWindow exists, with an existing-window check for reloads.
  • Reset capture-ready, host-alive, and bootstrap state during ShellHost teardown.
  • Moved next/previous off WH_MOUSE_LL and onto the router thread. Track navigation now uses TrySkipNextAsync / TrySkipPreviousAsync on the same current GSMTC session as play/pause.
  • Replaced the 100 ms desktop polling timer with a one-shot dismissal timer. A process-filtered WinEvent hook restores the card when Quick Settings opens, and the open-panel scan now checks the class before visibility/DWM work.
  • Documented why Explorer owns the taskbar gesture: it owns the input surface, remains available across Quick Settings-host restarts, filters non-shell Explorer instances at the taskbar-window boundary, supports the older ShellExperienceHost split, and avoids adding a separate tool process and IPC layer.
  • Retained the integrated gesture because this mod adds horizontal one-held-tilt semantics, fixed Ctrl + empty-taskbar middle-click, current-session routing, and the standalone genuine Windows media card as one behavior; the README keeps the explicit Taskbar Scroll Actions distinction.
  • Added the recorded GIF to the catalog README via the allowed raw.githubusercontent.com host. The public source/asset repository is https://github.com/shreyasjswork/taskbar-horizontal-scroll-media.

Optional cleanup included removing the unnecessary mutex no_destroy, making the reparent flag atomic, clamping non-positive settings to their documented minima, removing the unused UI Automation link library, simplifying taskbar root detection, and making UIA depth exhaustion fail closed.

Validation: zero-warning Windhawk compile, clean official pr_validation.py, GIF URL verified as 200 image/gif, one changed catalog file, one v1.0.0 commit, and byte-identical standalone source.

/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 6, 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 mod is well documented and the TAP boilerplate matches the reference mods, but the teardown path breaks Windhawk's unload contract and the input layer is heavier than it needs to be.

1. Wh_ModUninit can leave a permanent extra reference on the mod module. In ShutdownMediaCapture the code takes a reference on its own module and, on the 5-second timeout path, deliberately never releases it:

HMODULE keepAlive = nullptr;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, ..., &keepAlive);
...
if (wait == WAIT_OBJECT_0) {
    if (keepAlive) FreeLibrary(keepAlive);
} else {
    // Keep the image mapped if the UI dispatcher runs this callback late.

Windhawk expects exactly one reference on the mod module when Wh_ModUninit returns, and unloads it with a single FreeLibrary. With the extra reference that FreeLibrary becomes a no-op, so the image stays mapped with all its stale global state, and the consequences compound:

  • RestoreCapturedMedia() never runs, so Windows' own ControlCenter.MediaTransportControls stays parented into the mod's XAML island — Quick Settings permanently loses its media card until ShellHost/ShellExperienceHost restarts.
  • UnregisterCapturedHostClass() never runs, so kCapturedHostClass stays registered with an lpfnWndProc pointing into the mod image. On the next load RegisterClassExW fails with ERROR_CLASS_ALREADY_EXISTS, EnsureCapturedHostWindow() returns false, and the standalone card silently stops working for good.
  • The captured FrameworkElement / CoreDispatcher / DesktopWindowXamlSource globals are never released.

Additionally, even on the success path Wh_ModUninit can block for up to 5 seconds.

The fix is to make the teardown synchronous on the XAML UI thread instead of an async RunAsync you then have to outlive. windows-11-taskbar-styler uses a RegisterWindowMessage + SendMessage helper for exactly this — see RunFromWindowThread and its use in that mod's Wh_ModUninit. You already have a window on that thread (g_capturedHostWindow), and the ControlCenterWindow HWND otherwise. A SendMessage-based dispatch cannot be "late", so no module reference is needed and the timeout branch disappears entirely. (Note the mod holds no lock at that point, so the usual lock-and-SendMessage deadlock rule is satisfied — just keep it that way.)

2. The UI Automation object is released after RoUninitialize(). In RouterThreadProc:

HRESULT apartmentResult = RoInitialize(RO_INIT_MULTITHREADED);
bool uninitializeApartment = SUCCEEDED(apartmentResult);
winrt::com_ptr<IUIAutomation> automation;   // function scope
...
if (uninitializeApartment) RoUninitialize();
return 0;                                   // ~automation runs here

automation has function scope, so its Release() happens after the apartment has been torn down. Scope it, or release it explicitly:

automation = nullptr;
if (uninitializeApartment) RoUninitialize();

3. Unload can hang Explorer. StopRuntime() only posts WM_QUIT to the hook thread if g_hookThreadId has already been published:

if (DWORD threadId = g_hookThreadId.load())
    PostThreadMessageW(threadId, WM_QUIT, 0, 0);
if (g_hookThread) WaitForSingleObject(g_hookThread, INFINITE);

InitializeExplorerRole() gives up after WaitForSingleObject(g_hookReadyEvent, 1000) and returns false, which makes Wh_ModInit call StopRuntime(). If the hook thread hasn't reached g_hookThreadId = GetCurrentThreadId() yet, no WM_QUIT is posted; the thread then installs the hook and blocks in GetMessageW forever, and the INFINITE wait never returns — Wh_ModInit hangs Explorer permanently. Drive the hook thread's exit off g_stopEvent too (e.g. MsgWaitForMultipleObjects on g_stopEvent, and bail out before SetWindowsHookExW if g_shuttingDown is already set) rather than relying on the ID being published in time.

Related: the router thread is also joined with INFINITE while it can be blocked in cross-process calls it doesn't control — RequestAsync().get() / TrySkipNextAsync().get() into the media-session broker, and IUIAutomation::ElementFromPoint against a busy taskbar UI thread. Neither observes g_stopEvent once entered. Bounding those waits (e.g. IAsyncOperation::wait_for) so the loop can always get back to the stop check would make the unload path robust rather than best-effort.

4. A system-wide WH_MOUSE_LL hook for a taskbar-only gesture. Every mouse event on the machine — moves included — makes a synchronous round trip to the mod's hook thread, in every explorer.exe process the mod loads into, including ones that own no taskbar (InitializeExplorerRole() installs it unconditionally). That's input latency and blast radius for a gesture that only matters over this process's own taskbar, and it's why IsPointOverTaskbar / IsOwnedTaskbarWindow have to exist at all.

The repo convention is to handle this in the taskbar itself:

  • taskbar-scroll-actions subclasses Shell_TrayWnd/Shell_SecondaryTrayWnd and, on Windows 11, hooks the Windows.UI.Input.InputSite.WindowClass window proc to get WM_POINTERWHEELWM_POINTERHWHEEL (0x024F) is the horizontal counterpart.
  • taskbar-empty-space-clicks handles WM_MBUTTONDOWN in the taskbar subclass proc, and uses the same UIA ElementFromPoint trick for empty-space detection.

Both use WindhawkUtils::SetWindowSubclassFromAnyThread. Going that route also lets you drop the process/taskbar guards and the per-explorer-process duplication. If it turns out horizontal tilt genuinely doesn't reach those windows, please say so in the PR — but then at minimum the hook should only be installed in the process that actually owns a taskbar.

5. XAML diagnostics TAP conflict in ShellHost. windows-11-notification-center-styler injects its own TAP into the same ShellHost.exe / ShellExperienceHost.exe processes, and XAML diagnostics only supports one consumer at a time (that's the reason windows-11-taskbar-styler hooks InitializeXamlDiagnosticsEx to arbitrate). Please test this mod with the Notification Center Styler enabled and document the outcome — that's a very common combination.

6. Overlap with Taskbar Scroll Actions. The standalone Quick Settings card is genuinely new, and the README already explains the difference — thanks for that. But the gesture half (wheel over the taskbar → an action, with a modifier and a direction-reverse option) is exactly what taskbar-scroll-actions already provides as a configurable framework. Adding "horizontal wheel" as a scroll direction and "media next/previous/play-pause" as an action there would serve users better than a second mod that binds the taskbar wheel. Worth considering whether this mod should be only the native-card feature.

Optional improvements

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

  • Custom log wrappers. LogNativeState(int stage, HRESULT) / LogRouterState(int stage, DWORD) emit lines like Native state: stage=-731, result=0x.... Windhawk already provides logging with the mod name prefixed and an enable/disable toggle; the magic stage numbers mean nobody reading a user's log (including you, months from now) can tell what -731 was. Call Wh_Log directly with a descriptive message.
  • WindhawkUtils::StringSetting. In LoadSettings, replace the raw Wh_GetStringSetting + Wh_FreeStringSetting pair with the RAII wrapper. Also note Wh_GetStringSetting never returns NULL — it returns L"" — so the if (value) guard is dead code.
  • Dead parameter. IsQuickPanelOpen(HWND nativeHostToSkip = nullptr) and PanelSearchContext::nativeHostToSkip are never given a non-null value (the single call site is IsQuickPanelOpen() in RouterThreadProc, and it runs in a different process from the host window anyway). Drop them.
  • Redundant @compilerOptions libraries. -luser32 -lshell32 -lole32 -loleaut32 -luuid are linked by default; -lwindowsapp already covers what -lruntimeobject provides here. Only -ldwmapi, -lshcore and -lwindowsapp look load-bearing.
  • Duplicated bodies. ToggleCurrentMediaSession and SkipCurrentMediaSession are the same function apart from one line; they could share a helper that takes the operation. Similarly, ClampSetting's if (value <= 0) return minimum; is subsumed by the value < minimum check below it.
  • Non-atomic globals read from the uninit thread. g_capturedHostWindow, g_capturedIslandWindow, g_capturedHostClassRegistered and g_capturedHostInstance are owned by the XAML UI thread but read from ShutdownMediaCapture on whatever thread Wh_ModUninit runs on. Only reachable at unload, so low impact — and moving the teardown to a synchronous RunFromWindowThread (item 1) removes it for free.
  • Wh_ModInit blocks up to 1 second waiting for g_hookReadyEvent, which delays Explorer startup in the worst case. Consider not blocking, and treating hook-install failure asynchronously.
  • IPC event ACLs. The Local\Windhawk.TaskbarHorizontalScrollMedia.* events are created with a default DACL, so any medium-integrity process in the session can signal ...ShowNativeControl.v4 and pop the media card. Harmless in practice, but worth a comment or a tighter DACL if you care.

Functionality notes

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

  • Reparenting Windows' live element is inherently fragile. While the standalone card is up (up to 15 s), the real Quick Settings panel has no media control, and correctness depends on every restore path firing: the kDismissTimer, the EVENT_OBJECT_SHOWg_panelShownEvent path, the Remove mutation path, and the unload path. Any one that doesn't run leaves Quick Settings visually broken until the host process restarts. There's no obviously cleaner way to show the genuine card standalone, so this is an FYI rather than a required change — but it's worth stress-testing (open/close Quick Settings while the card is up, restart ShellHost mid-display, toggle the mod repeatedly).
  • The bootstrap opens and hides the real Quick Settings window. ShellExecuteW(L"ms-controlcenter:") followed by ShowWindow(panelWindow, SW_HIDE) steals focus and will dismiss the Start menu, an open flyout, or an IME candidate window if the user has one up. It also leaves Control Center believing it's shown, so the next Win+A may just toggle it "closed" with nothing visible. The README mentions the flash; the focus/toggle side effects are worth mentioning too.
  • Card-bounds heuristic. FindNativeCardBounds picks the largest child within 85–100% of the root width, preferring Windows.UI.Xaml.Controls.Border with a × 10 score bonus. That's a lot of tuning against a private tree; a template change could pick the wrong element and produce a mis-cropped popup rather than a clean failure. A sanity check (e.g. reject and fall back to the uncropped root if the chosen bounds look degenerate) would degrade more gracefully.
  • Request ordering. WaitForMultipleObjects returns the lowest signaled index, and g_routeRequestEvent sits at index 1 while g_trackRequestEvent is at index 3. So a track request that arrives while the router is inside WaitForSingleObject(g_stopEvent, flyoutDelayMs) (600 ms by default) waits behind it. Rapid successive tilts will feel laggy.
  • Re-show doesn't reposition. The early-return in ShowCapturedMediaOnUiThread (if (g_originalMediaParent) { ShowWindow(...); SetTimer(...); return; }) re-shows the card at its previous coordinates. If the user moved to another monitor since the last gesture, it appears on the old one.
  • Bottom-taskbar assumption. GetBottomTaskbarTop / FindBottomTaskbarBoundsProc only recognise a taskbar in the lower half of the monitor and fall back to rcWork.bottom otherwise. Fine for stock Windows 11, but the card will be placed at the bottom of the work area for users who move the taskbar with another mod.


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 6, 2026
@shreyasjswork
shreyasjswork force-pushed the agent/add-taskbar-horizontal-scroll-media branch from 8f09c57 to 8c50bf9 Compare August 6, 2026 08:54
@shreyasjswork

Copy link
Copy Markdown
Author

Addressed the review against the updated v1.0.0 commit:

  • Removed the timeout/module-reference teardown entirely. Native-card cleanup now runs synchronously on the owning XAML thread: the private host handles a synchronous cleanup message in its own WndProc, with the repository's RunFromWindowThread pattern used through ControlCenterWindow when no private host exists. Restore, XAML-source close, host destruction, class unregister, and thread-affine releases all finish before Wh_ModUninit returns.
  • Moved UI Automation out of RouterThreadProc. It is lazily created and used on the taskbar InputSite UI thread, then synchronously released on that same thread during unload; the router now tears down its apartment only after all of its own work is gone.
  • Removed the low-level mouse hook and hook thread completely, including the startup/quit race.
  • The Explorer input path now follows the repository taskbar convention: it hooks CreateWindowInBand, identifies only taskbar-owned Windows.UI.Input.InputSite.WindowClass, and hooks that window procedure for WM_POINTERHWHEEL and WM_POINTERDOWN. Other Explorer processes have no taskbar InputSite and stay inert; there is no system-wide per-mouse-event callback.
  • Kept the taskbar WndProc lightweight: horizontal input only updates latch state and signals the router; Ctrl + middle-click performs the same UIA empty-space check used by the repository taskbar click implementation, then signals the router. GSMTC calls never run on the taskbar thread.
  • Replaced blocking WinRT .get() calls with two-second wait_for bounds plus cancellation for manager, play/pause, next, and previous operations. Router unload therefore returns from broker work to the stop check within a defined bound.
  • Reworked the flyout delay into a router deadline instead of a blocking sleep, so media commands remain responsive while a card show is pending.
  • Treated simultaneous Windows 11 Notification Center Styler use as unsupported rather than claiming coexistence. The README documents the single-TAP conflict and its outcome: track controls continue, standalone capture depends on which TAP attaches first, and one mod plus the affected shell host must be restarted. Notification Center Styler is not installed on this isolated test system, so no unperformed runtime coexistence test is claimed.
  • Retained the integrated gesture because the taskbar InputSite owns the horizontal/pointer gesture while the Explorer router and Quick Settings host jointly provide current-session navigation and the standalone genuine card. Splitting it would add another catalog dependency and IPC contract without removing either required in-process role.

Optional cleanup also replaced opaque numeric-stage log wrappers with descriptive Wh_Log messages, removed the dead Quick Settings skip parameter and redundant clamp branch, and corrected the shared-event ACL comment.

Validation: zero-warning Windhawk compile against the installed 1.7.3 engine import library, clean official pr_validation.py, git diff --check, one changed catalog source file, one v1.0.0 commit, byte-identical standalone source, and the public GIF remains available from the allowed raw GitHub host.

/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 6, 2026
@shreyasjswork
shreyasjswork force-pushed the agent/add-taskbar-horizontal-scroll-media branch from 8c50bf9 to a9085bc Compare August 6, 2026 08:56
@shreyasjswork

Copy link
Copy Markdown
Author

One final teardown ordering refinement is included in a9085bca: the synchronous XAML-thread cleanup destroys the host window, returns from its WndProc, and only then unregisters the process-wide host class on the unload thread. This ensures no host WndProc remains on the stack during class removal.

The full response to the preceding review is in the immediately previous comment. Compile and catalog validation remain clean.

/ai-review

@windhawk-reviewer

Copy link
Copy Markdown

@shreyasjswork /ai-review can't be applied here: an AI review was already requested, please wait for it to be posted.

@windhawk-reviewer

Copy link
Copy Markdown

This pull request has already had 3 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-06 13:07 UTC (in 4 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 6, 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