From 059986ebce4900a7f8e46802aed903f3a0526579 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Fri, 1 May 2026 14:56:19 -0700 Subject: [PATCH] [Unpackaged] Fix taskbar glomming due to AUMID (#20064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bug was caused by an AUMID mismatch between the Taskbar's .lnk file and Windows Terminal. Since no AUMID was associated with the .exe, the OS automatically creates one for us. However, #20018 added an AUMID for unpackaged scenarios, so now there was a mismatch, resulting in a new taskbar entry being created. To fix this, we check if a .lnk points to our .exe in the taskbar. There's 3 cases here: 1. no .lnk of interest exists --> set the AUMID normally 2. the .lnk carries our AUMID --> set the AUMID normally 3. the .lnk doesn't have an AUMID (aka uses the auto-resolved one) --> - for this launch: don't set the AUMID so that we both use the auto-resolved one - on next launch: set the AUMID on the .lnk and process so that they all agree ## Validation Steps Performed In unpackaged folder, move WindowsTerminal.exe to the taskbar (creates .lnk)... ✅ Double-click the .exe --> Same taskbar entry is used ✅ Double-click the .exe _again_ --> second window goes to same taskbar entry The first window doesn't have to close for this to work. It just * works *! Bug introduced in #20018 Closes #20053 --- .../WindowsTerminal/WindowEmperor.cpp | 128 +++++++++++++++++- src/cascadia/WindowsTerminal/WindowEmperor.h | 3 + 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 917f492be9..82d3c82620 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include "AppHost.h" #include "resource.h" @@ -313,6 +315,107 @@ AppHost* WindowEmperor::_mostRecentWindow() const noexcept return mostRecent; } +// GH#20053: The shell resolves taskbar grouping identity as: per-window AUMID > +// per-process AUMID > auto-derived from exe path. Before we started setting a +// process AUMID, both the pinned .lnk and the process used auto-derived +// identity, so they matched. Now that we set an explicit AUMID, a pinned .lnk +// that predates the AUMID change has no AUMID and still uses auto-derived +// identity, causing a mismatch and a duplicate taskbar button. +// +// To fix this, we check if a pinned taskbar shortcut (.lnk) points to our exe. +// If it already carries our AUMID (or no pin exists), we set the process AUMID +// normally. If a pin exists WITHOUT our AUMID, we skip setting the process +// AUMID for THIS launch (both sides use auto-derived identity, so they match) +// and defer stamping the shortcut to process exit. On the next launch, the pin +// has our AUMID, so we set the process AUMID to match, and both agree. +// +// NOTE: On the first launch after pinning, the process AUMID is not set. If +// toast notifications are needed in the future, use +// ToastNotificationManager::CreateToastNotifier(aumid) with the AUMID string +// directly. That API does not depend on SetCurrentProcessExplicitAppUserModelID. +// A Start Menu shortcut with the AUMID (separate from the taskbar pin) is also +// required for toast routing; see +// https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/send-local-toast-other-apps +void WindowEmperor::_setupAumid(const std::wstring& aumid) +{ + const auto ourExePath = wil::GetModuleFileNameW(nullptr); + + bool needsDeferredStamping = false; + std::wstring pinnedLnkPath; + + const auto taskbarGlob = wil::ExpandEnvironmentStringsW( + LR"(%APPDATA%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\*.lnk)"); + + WIN32_FIND_DATAW findData{}; + const wil::unique_hfind findHandle{ FindFirstFileExW(taskbarGlob.c_str(), FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH) }; + if (findHandle) + { + const auto lastSlash = taskbarGlob.rfind(L'\\'); + const auto taskbarDir = taskbarGlob.substr(0, lastSlash + 1); + + do + { + const auto lnkPath = taskbarDir + findData.cFileName; + + wil::com_ptr shellLink; + if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)))) + { + continue; + } + + const auto persistFile = shellLink.try_query(); + if (!persistFile || FAILED(persistFile->Load(lnkPath.c_str(), STGM_READ))) + { + continue; + } + + wchar_t targetPath[MAX_PATH]{}; + if (FAILED(shellLink->GetPath(targetPath, MAX_PATH, nullptr, SLGP_RAWPATH))) + { + continue; + } + + if (til::compare_ordinal_insensitive(targetPath, ourExePath) != 0) + { + continue; + } + + // Found a pin pointing to us. Assume it needs stamping unless + // we confirm it already has our AUMID. + pinnedLnkPath = lnkPath; + needsDeferredStamping = true; + + if (const auto propertyStore = shellLink.try_query()) + { + wil::unique_prop_variant pv; + if (SUCCEEDED(propertyStore->GetValue(PKEY_AppUserModel_ID, &pv)) && + pv.vt == VT_LPWSTR && pv.pwszVal && + aumid == pv.pwszVal) + { + needsDeferredStamping = false; + } + } + + break; + } while (FindNextFileW(findHandle.get(), &findData)); + } + + if (needsDeferredStamping) + { + // The pin exists but doesn't have our AUMID yet. Don't set the process + // AUMID or stamp the shortcut now. Writing the shortcut causes the + // shell to re-read it immediately, changing the pin's cached identity + // mid-launch and creating a mismatch in the opposite direction. Instead, + // stamp it at shutdown when the taskbar association no longer matters. + _pendingAumidLnkPath = std::move(pinnedLnkPath); + _pendingAumid = aumid; + } + else + { + LOG_IF_FAILED(SetCurrentProcessExplicitAppUserModelID(aumid.c_str())); + } +} + void WindowEmperor::HandleCommandlineArgs(int nCmdShow) { // When running without package identity, set an explicit AppUserModelID so @@ -373,7 +476,7 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) #else fmt::format_to(std::back_inserter(unpackagedAumid), FMT_COMPILE(L".{:08x}"), hash); #endif - LOG_IF_FAILED(SetCurrentProcessExplicitAppUserModelID(unpackagedAumid.c_str())); + _setupAumid(unpackagedAumid); } } @@ -553,6 +656,29 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) Shell_NotifyIconW(NIM_DELETE, &_notificationIcon); } + // GH#20053: Deferred shortcut stamping. See _setupAumid() for context. + if (!_pendingAumidLnkPath.empty()) + { + wil::com_ptr shellLink; + if (SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)))) + { + if (const auto persistFile = shellLink.try_query(); + persistFile && SUCCEEDED(persistFile->Load(_pendingAumidLnkPath.c_str(), STGM_READWRITE))) + { + if (const auto propertyStore = shellLink.try_query()) + { + wil::unique_prop_variant pv; + if (SUCCEEDED(InitPropVariantFromString(_pendingAumid.c_str(), &pv)) && + SUCCEEDED(propertyStore->SetValue(PKEY_AppUserModel_ID, pv)) && + SUCCEEDED(propertyStore->Commit())) + { + persistFile->Save(_pendingAumidLnkPath.c_str(), TRUE); + } + } + } + } + } + // There's a mysterious crash in XAML on Windows 10 if you just let _app get destroyed (GH#15410). // We also need to ensure that all UI threads exit before WindowEmperor leaves the scope on the main thread (MSFT:46744208). // Both problems can be solved and the shutdown accelerated by using TerminateProcess. diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index 42c84c38c9..80d87023d7 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -69,6 +69,7 @@ private: void _persistState(const winrt::Microsoft::Terminal::Settings::Model::ApplicationState& state) const; void _finalizeSessionPersistence() const; void _checkWindowsForNotificationIcon(); + void _setupAumid(const std::wstring& aumid); wil::unique_hwnd _window; winrt::TerminalApp::App _app{ nullptr }; @@ -84,6 +85,8 @@ private: std::optional _currentSystemThemeIsDark; int32_t _windowCount = 0; int32_t _messageBoxCount = 0; + std::wstring _pendingAumidLnkPath; + std::wstring _pendingAumid; #if 0 // #ifdef NDEBUG static constexpr void _assertIsMainThread() noexcept