Add Taskbar collapse to running apps mod - #5217
Conversation
|
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 See the pull request review process for the full details. |
|
/ai-review |
Submission reviewNote: 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. Nice, self-contained mod — the taskbar hooks are narrow, the settings all map to code, and the reveal/animation state machine is well commented. The findings below are mostly around unload safety and per-tick cost. 1. Two problems combine here:
A mod must be fully unloadable the moment its teardown returns, so the wait has to be unconditional. Fix both: create a ready event, force the queue with // In HotkeyThreadProc, before anything slow:
MSG msg;
PeekMessageW(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
SetEvent(g_readyEvent);
...
// In StopHotkeyThread:
WaitForSingleObject(g_readyEvent, INFINITE);
PostThreadMessage(g_hotkeyThreadId, WM_QUIT, 0, 0);
WaitForSingleObject(g_hotkeyThread, INFINITE);2.
Wrap it and keep releasing explicitly on the owning thread: [[clang::no_destroy]] std::optional<std::unordered_map<void*, FrameContext>>
g_frames{std::in_place};Accesses become 3. The leftover sweep in The final
4. 20 Hz visual-tree polling on Explorer's UI thread is the default, and it rarely sleeps.
The sleep mode doesn't help as much as it looks:
"Is this app running" changes at human speed; 250–500 ms would be plenty as a default (the grace-period countdown is the only thing that wants a fast tick, and that only matters while revealed). Suggest raising the default, and consider keeping the fast rate only while 5.
6. Remove the custom file logger.
7. Add a screenshot or GIF to the README. The whole point of the mod is a visible taskbar change with an animation, and there's currently no image. A short GIF of the collapse/reveal would help a lot on windhawk.net. Allowed hosts are Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
Fixed a small bug with the closing animation glitching mid transition. Fixed all concerns raised by the ai-review and ran another adverserial Fable 5 review
|
/ai-review |
Submission reviewNote: 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. Round 2. All seven items from the previous review are genuinely fixed — the hotkey thread now has a ready event and unconditional waits, 1. The hotkey thread and the Start-menu watcher start in every
The consequence is not just wasted work:
Gate the thread on the taskbar actually being present. The cheapest hook is the flag you already maintain: start it where // Wh_ModInit: replace the unconditional StartHotkeyThread() with a call from
// each place that sets g_taskbarViewDllLoaded.
void Wh_ModSettingsChanged() {
bool hadHotkeyThread = g_hotkeyThread != nullptr;
StopHotkeyThread();
LoadSettings();
if (hadHotkeyThread || g_taskbarViewDllLoaded) {
StartHotkeyThread();
}
g_instantApplyGen++;
}If you'd rather key on the window than the DLL, 2. Unload still has two five-second give-up paths that let the DLL unmap with mod code queued. This is the same class of defect as the hotkey-thread wait from the last round, now in the dispatcher paths:
Both waits can be unconditional, and by the time you reach them you have already earned that: if (!queued || WaitForSingleObject(done, INFINITE) == WAIT_OBJECT_0) {
CloseHandle(done);
}
...
while (g_pendingWakes.load() > 0) {
Sleep(10);
}One residual note while you're in there: 3. Windows' task-button transitions stay stripped while the collapse feature is switched off.
if (!g_unloading) {
for (auto& button : buttons) {
DeanimateButton(ctx, button);
}
}
if (g_unloading) {
// leave as-is
} else if (!g_collapseEnabled && ctx.hiddenByUs.empty()) {
ReanimateButtons(ctx);
} else {
for (auto& button : buttons) {
DeanimateButton(ctx, button);
}
}(Keep deanimating while Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
'nother round. Following things from last rounds review were not addressed on purpose. Heres what claude wrote (and where i interjected a little); Two things I did NOT do, on purpose The "detection broken → hide nothing" guard (functionality note 2): the suggested heuristic is wrong as stated. A taskbar where zero apps are running also has zero resolvable indicators and zero marker matches — that's the legitimate all-idle state, where the correct action is to hide everything. MIght change it later. Touch/README note: showing you the line before writing it, per your rules. Proposed addition under Notes: * Reveal triggers follow the mouse pointer; touch and pen input are not tracked — it's a feature, not a fix.
|
'nother round. Following things from last rounds review were not addressed on purpose. Heres what claude wrote (and where i interjected a little); Two things I (Claude) did NOT do, on purpose Touch/README note: showing you the line before writing it, per your rules. Proposed addition under Notes: * Reveal triggers follow the mouse pointer; touch and pen input are not tracked — it's a feature, not a fix. Also save for later if the need should arise! |
|
/ready-for-reviewer |
|
/ai-review |
Submission reviewNote: 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. Round 3. The previous round's items are addressed: the hotkey thread is now gated on 1. The teardown path can escape as an exception, and both unbounded waits in
Separately, that same loop is reached precisely when a context's thread has no windows at all ( // CleanupOnThisThread: make the per-frame work exception-safe.
for (void* key : keys) {
auto it = g_frames->find(key);
if (it == g_frames->end()) continue;
try {
if (it->second.timer) {
it->second.timer.Stop();
it->second.timer.Tick(it->second.tickToken);
}
RestoreFrame(it->second);
} catch (winrt::hresult_error const&) {
}
g_frames->erase(it); // must still run, or the straggler sweep loops on it
}
// Straggler lambda: signal unconditionally.
dispatcher.RunAsync(High, [done]() {
try { CleanupOnThisThread(); } catch (...) {}
SetEvent(done);
});
// And skip the wait for a thread that is already gone:
if (HANDLE hThread = OpenThread(SYNCHRONIZE, FALSE, threadId)) {
bool exited = WaitForSingleObject(hThread, 0) == WAIT_OBJECT_0;
CloseHandle(hThread);
if (exited) continue; // nothing on that thread can ever run our lambda
}2. Every pointer-move event over the taskbar runs three full XAML hit-tests plus four screen-space window lookups. With
Pointer moves arrive at 100–1000 Hz, so with ~15 pinned apps this is a few hundred tree hit-tests and a couple of thousand HSTRING allocations per second on the taskbar UI thread — enough to show up as taskbar input lag and dropped frames, which is the one thread where that is most visible. (The default The expensive half only decides whether the reveal is allowed to fire, not whether the dwell should be armed, so it can be deferred. Keep the cheap checks ( ULONGLONG now = GetTickCount64();
ULONGLONG since = g_emptyHoverSinceTick;
if (since == 0) { g_emptyHoverSinceTick = now; return; }
if (now - since < (ULONGLONG)g_settings.revealDelayMs) return;
if (!PointerClearsNeighbouringSurfaces(key, args)) { // the expensive half
g_emptyHoverSinceTick = 0;
return;
}
g_revealed = true;
Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
NOTHER ROUND
|
/ai-review |
Had some time whilst ai-review was on cooldown, so addressed everything and made a few tweaks! Should be solid now
|
/ai-review |
Submission reviewNote: 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. Round 4. The previous round's items are addressed: the per-move path is cheap again for the default 1. Running detection should query the taskbar's own
using TaskListButton_get_IsRunning_t = HRESULT(WINAPI*)(void* pThis, bool* running);
TaskListButton_get_IsRunning_t TaskListButton_get_IsRunning_Original;
bool TaskListButton_IsRunning(FrameworkElement taskListButtonElement) {
bool isRunning = false;
TaskListButton_get_IsRunning_Original(
winrt::get_abi(
taskListButtonElement.as<winrt::Windows::Foundation::IUnknown>()),
&isRunning);
return isRunning;
}
// in the symbol hook array:
{
{LR"(public: virtual int __cdecl winrt::impl::produce<struct winrt::Taskbar::implementation::TaskListButton,struct winrt::Taskbar::ITaskListButton>::get_IsRunning(bool *))"},
&TaskListButton_get_IsRunning_Original,
},That removes 2. The permanent 250 ms / 1000 ms poll can be event-driven, and the poll rate is user-visible. Every registered frame runs a The rate is also a correctness-visible number, not just a cost. The mod is what sets Taskbar.View.dll raises this for you: {
{LR"(private: void __cdecl winrt::Taskbar::implementation::TaskListButton::UpdateVisualStates(void))"},
&TaskListButton_UpdateVisualStates_Original,
TaskListButton_UpdateVisualStates_Hook,
},Apply from there (plus the pointer hooks you already have), and keep the 3. Two unbounded waits in Both fallback paths block with
Both are tail cases, but the failure is a hung Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
|
/ai-review |
Submission reviewNote: 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. Round 5. The previous round's items landed well: running state now comes from 1. The two "give up and retry" paths never converge, so the 100 ms timer can stay running forever. The mod's headline property is that an idle taskbar costs nothing ( a) The apply retry budget re-arms itself. Lines 1156-1164: if (ApplyToFrame(*ctx, desired, instant)) {
ctx->instantGenSeen = instantGen;
ctx->applyRetries = 0;
} else if (ctx->applyRetries > 0) {
ctx->applyRetries--;
} else if (instant || ctx->appliedCollapse < 0) {
ctx->applyRetries = 20;
}
A // FrameContext
uint64_t retriesArmedGen = UINT64_MAX;
// OnTimerTick
} else if (ctx->applyRetries > 0) {
ctx->applyRetries--;
} else if (ctx->retriesArmedGen != instantGen &&
(instant || ctx->appliedCollapse < 0)) {
ctx->applyRetries = 20;
ctx->retriesArmedGen = instantGen; // one budget per generation
}b) The animation watchdog aborts without landing the target state. Lines 1146-1150: if (ctx->animActive &&
NowMs() - ctx->animStartMs >
(double)g_settings.animationDurationMs * 3.0 + 1000.0) {
StopAnimation(*ctx);
}The watchdog exists for the case where Make the watchdog terminal by landing the state it was animating toward: if (ctx->animActive && NowMs() - ctx->animStartMs > ...) {
// Land the target, or the next apply starts the same animation again.
if (ctx->animKind == AnimKind::Accordion && !ctx->animSwapped) {
SetCollapseState(*ctx, ctx->animButtons, ctx->animTargetCollapse);
}
StopAnimation(*ctx);
}Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
|
/ai-review |
Submission reviewNote: 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 unusually careful for its size — weak refs throughout, 1. Nothing discovers taskbars that already exist when the mod loads. A So when the mod is enabled mid-session (the normal case — a user installs it and the taskbar is already up and idle), nothing collapses until the taskbar happens to re-measure or the cursor moves over it. Same for the first settings change after that. Please verify by enabling the mod and not touching the mouse. The established pattern is an explicit apply on the taskbar thread at 2.
Ideally this becomes something the user opts into rather than a side effect of installing the mod. 3. Unload path: a stray timer stays armed, and the final drain is unbounded. Two separate points, both in the teardown:
Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
|
/ai-review |
Submission reviewNote: 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 plumbing is in good shape — settings match the code exactly, Teardown gives up instead of guaranteeing it finished. The unload drain can spin forever. The last wait is unbounded: while (g_pendingWakes.load() > 0) {
Sleep(10);
}
The mod is ~3,000 lines for "hide pinned icons whose app isn't running", and roughly two thirds of that is a bespoke animation engine. Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
Adds a new mod: taskbar-collapse-to-running
Hides pinned taskbar icons whose app isn't running, so a taskbar full of pinned
shortcuts collapses down to just what's in use. Icons are only hidden and
unhidden in place, so pinned order is never changed.
Revealing is configurable: left-click the empty part of the taskbar (default),
hover, hover-once-the-cursor-slows ("rest"), a hotkey, or while the Start menu
is open. Windows' own icon reorder slide is stripped and replaced with an
optional spacing-only animation, including an eased gap-close when a running
app exits.
Tested on Windows 11 build 26200, left-aligned taskbar, single monitor.
Changelog
If this pull request updates an existing mod, describe the changes below:
Mod authorship
If this pull request introduces a new mod, please complete the section below.
This mod was created by:
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.