You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Resolves an issue where pinning modern multi-window and XAML Island applications (such as Windows Terminal, modern File Explorer instances, and hosted Chromium/Electron frames) only pins isolated window instances rather than the entire application.
Problem
Modern Windows applications hosting XAML controls inside Win32 frames assign per-window sub-AppUserModelIDs formatted as <Package>!App~Wh~w<HEX_HWND> (for example, Microsoft.WindowsTerminal_8wekyb3d8bbwe!App~Wh~w010E0A34).
Because Windows Shell's internal IVirtualDesktopPinnedApps COM interface performs exact string matching (wcscmp) against registered AppIDs:
Sub-AUMID Isolation: Calling PinAppID() with the raw window ID registers only that single transient window handle. Sibling windows under the same application remain unpinned, and closing the window leaves orphaned registry entries under HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops\PinnedApps.
Shell String Mismatch: Pinning only the base application ID fails to display existing secondary windows across desktops because their view identities carry the ~Wh~ suffix.
Pointer Conversion Discrepancy: On 64-bit runtimes, GetAppUserModelId() can return a raw pointer address rather than a decoded string, causing type errors downstream.
Solution
Decoded App ID & Canonical Base ID:
In AppView.app_id, ensure raw pointer values are resolved to strings using ctypes.wstring_at(raw).
Added AppView.base_app_id, which strips ~Wh~ window-hosting tokens to extract the canonical application identifier.
Hierarchical Pinning and Unpinning:
In AppView.pin_app(): Registers the base_app_id with PinAppID() and explicitly pins active sub-views belonging to that base ID via in-memory PinView(). This prevents orphaned registry keys while ensuring all active windows appear across workspaces.
In AppView.unpin_app(): Unpins both the base ID and window-specific IDs, and unpins matching active views.
In AppView.is_app_pinned(): Evaluates both base_app_id and raw app_id against IsAppIdPinned().
Desktop Transition Synchronization:
Added sync_pinned_apps(), exported in pyvda.__init__. It inspects open views and pins individual sub-views for applications whose base ID is pinned.
Called sync_pinned_apps() inside VirtualDesktop.go() prior to switching desktops so secondary windows stay visible across desktops.
Testing:
Added unit tests in tests/test_desktop_functions.py validating base_app_id parsing and sync_pinned_apps() execution.
Background Context & Deeper Architectural Notes
This fix is scoped to resolve the immediate XAML Island sub-AUMID bug cleanly with zero breaking changes.
For maintainers interested in deeper architectural context, we have documented empirical investigations covering the Windows Virtual Desktop subsystem:
Native Shell Mechanics: An analysis of how explorer.exe (twinui.pcshell.dll) manages Task View, the boundary between Microsoft's public SDK (IVirtualDesktopManager) and undocumented interfaces (IVirtualDesktopManagerInternal), and why out-of-process ALPC calls behave differently than in-process shell code.
Cross-Repository Analysis: Comparative findings across pyvda, VirtualDesktopAccessor (Rust), WinStasis (C# .NET 10), and ADCE (Active Desktop Context Engine), noting that VirtualDesktopAccessor currently shares this exact ~Wh~ sub-AUMID pinning limitation.
COM Lifecycle Models: Analysis comparing reactive exception retry decorators (@_com_retry) and broadcast listeners (TaskbarCreated) against call-scoped, zero-cached-state transient invocation models.
Technical Note: Windows Shell Reverse Engineering & Task View Parity
To provide additional architectural context for maintainers evaluating this change, we disassembled the Windows Shell implementation in twinui.pcshell.dll using Microsoft symbol data (twinui.pcshell.pdb, Build 26200) to inspect how native Task View handles multi-window and XAML Island application pinning.
1. Why PinAppID Fails on Hosted Windows
In twinui.pcshell.dll, VirtualPinnedAppsHandler::PinAppID delegates view lookup to CApplicationViewManager::GetViewsByAppUserModelId. This routine iterates active views and compares IDs via IApplicationView::IsEqualByAppUserModelId (RVA 0x180130ba0), which calls:
Applications using XAML Islands or detached frames append dynamic sub-identifiers formatted as <Package>!App~Wh~w<HEX_HWND>. Because CompareStringOrdinal requires exact string equality without prefix matching:
Passing the canonical AppID causes string comparison to fail against views bearing ~Wh~w<HWND>, returning zero views to pin.
Passing the raw AppID pins only that single transient window handle, polluting HKCU\...\VirtualDesktops\PinnedApps with stale HWND values upon exit.
Furthermore, VirtualPinnedAppsHandler::ViewAddedInternal (RVA 0x1801b91c0) also executes CompareStringOrdinal against the pinned list when a new window spawns. Because the new instance carries a distinct HWND suffix, the OS never matches it to a previously pinned base AppID.
2. How Native Task View Handles This
When selecting "Show windows from this app on all desktops" in Task View, modern Windows does not rely on PinAppID alone. Task View routes to DesktopTaskGroupsSwitchItemController::PinUnpinToAllDesktops (RVA 0x1805bfb74):
0x1805bfbf5: mov r15, qword ptr [rbp - 0x10] ; Window array from SnapAssistSnappedWindows
0x1805bfc10: mov ebx, dword ptr [r14] ; Current HWND in group
0x1805bfc35: call qword ptr [rax + 0x30] ; GetViewForHwnd
0x1805bfc71: mov rax, qword ptr [rax + 0x38] ; Vtable slot 7: PinView
0x1805bfc75: call rax ; PinView invoked on each window view
0x1805bfc84: add r14, 4 ; Loop across all windows in group
Task View resolves this by enumerating the application's active window handles and invoking PinView individually on each view.
This PR mirrors the native Task View execution path:
PinAppID(base_app_id) registers the canonical package identity persistently without transient HWND tokens.
view.pin() (PinView) is called on active sub-views sharing that base ID.
sync_pinned_apps() synchronizes newly opened secondary windows during desktop switches, bridging the gap left by CompareStringOrdinal in ViewAddedInternal.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary of Changes
Resolves an issue where pinning modern multi-window and XAML Island applications (such as Windows Terminal, modern File Explorer instances, and hosted Chromium/Electron frames) only pins isolated window instances rather than the entire application.
Problem
Modern Windows applications hosting XAML controls inside Win32 frames assign per-window sub-AppUserModelIDs formatted as
<Package>!App~Wh~w<HEX_HWND>(for example,Microsoft.WindowsTerminal_8wekyb3d8bbwe!App~Wh~w010E0A34).Because Windows Shell's internal
IVirtualDesktopPinnedAppsCOM interface performs exact string matching (wcscmp) against registered AppIDs:PinAppID()with the raw window ID registers only that single transient window handle. Sibling windows under the same application remain unpinned, and closing the window leaves orphaned registry entries underHKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops\PinnedApps.~Wh~suffix.GetAppUserModelId()can return a raw pointer address rather than a decoded string, causing type errors downstream.Solution
Decoded App ID & Canonical Base ID:
AppView.app_id, ensure raw pointer values are resolved to strings usingctypes.wstring_at(raw).AppView.base_app_id, which strips~Wh~window-hosting tokens to extract the canonical application identifier.Hierarchical Pinning and Unpinning:
AppView.pin_app(): Registers thebase_app_idwithPinAppID()and explicitly pins active sub-views belonging to that base ID via in-memoryPinView(). This prevents orphaned registry keys while ensuring all active windows appear across workspaces.AppView.unpin_app(): Unpins both the base ID and window-specific IDs, and unpins matching active views.AppView.is_app_pinned(): Evaluates bothbase_app_idand rawapp_idagainstIsAppIdPinned().Desktop Transition Synchronization:
sync_pinned_apps(), exported inpyvda.__init__. It inspects open views and pins individual sub-views for applications whose base ID is pinned.sync_pinned_apps()insideVirtualDesktop.go()prior to switching desktops so secondary windows stay visible across desktops.Testing:
tests/test_desktop_functions.pyvalidatingbase_app_idparsing andsync_pinned_apps()execution.Background Context & Deeper Architectural Notes
This fix is scoped to resolve the immediate XAML Island sub-AUMID bug cleanly with zero breaking changes.
For maintainers interested in deeper architectural context, we have documented empirical investigations covering the Windows Virtual Desktop subsystem:
explorer.exe(twinui.pcshell.dll) manages Task View, the boundary between Microsoft's public SDK (IVirtualDesktopManager) and undocumented interfaces (IVirtualDesktopManagerInternal), and why out-of-process ALPC calls behave differently than in-process shell code.pyvda,VirtualDesktopAccessor(Rust),WinStasis(C# .NET 10), andADCE(Active Desktop Context Engine), noting thatVirtualDesktopAccessorcurrently shares this exact~Wh~sub-AUMID pinning limitation.@_com_retry) and broadcast listeners (TaskbarCreated) against call-scoped, zero-cached-state transient invocation models.Research documents: