Skip to content

Fix: identify browser web apps and Store apps by AppUserModelID - #4

Open
luisgaertner wants to merge 5 commits into
marvintrvl:mainfrom
luisgaertner:fix/pwa-window-identity
Open

Fix: identify browser web apps and Store apps by AppUserModelID#4
luisgaertner wants to merge 5 commits into
marvintrvl:mainfrom
luisgaertner:fix/pwa-window-identity

Conversation

@luisgaertner

@luisgaertner luisgaertner commented Jul 20, 2026

Copy link
Copy Markdown

Description

Several kinds of window could not be identified correctly, because WindowAnchor identifies windows by executable path and window class only. That is not enough for windows that share an executable with something else.

Browser web apps. A web app installed from Chrome or Brave (e.g. Insilico Terminal) is not a separate process - it runs inside chrome.exe / brave.exe and uses the same window class (Chrome_WidgetWin_1) as an ordinary browser window. So a PWA window was saved as "a Chrome window", and on restore BuildProcessStartInfo took the browser branch and started chrome.exe --restore-last-session, producing a plain browser window instead of the app. The process command line does not help either: launching a PWA (chrome_proxy.exe --app-id=...) only creates a window inside the already-running browser process.

Store apps. Apps installed from the Microsoft Store live under C:\Program Files\WindowsApps. Starting their executable by path runs them without package identity, so they cannot reach their packaged settings container - TradingView reopened in light theme instead of the user's dark theme.

Two windows of the same app. Two TradingView charts (or two Explorer windows) share executable, window class and - for packaged apps - AppUserModelID, so restore could not tell them apart and sometimes placed each on the other's monitor.

Single-site browser windows. Sites without a PWA manifest cannot be installed as an app, but users still keep them in a dedicated window beside a normal multi-tab window. Such a window is indistinguishable from any other browser window, and --restore-last-session restores a session rather than a specific window.

Approach

The AppUserModelID is the missing per-window identity. Chromium assigns every web-app window its own AUMID - that is how an installed web app gets its own taskbar group - and writes the same AUMID onto the Start-Menu shortcut it creates at install time. Packaged apps expose a PackageFamilyName!AppId AUMID via their process.

  • Capture - SHGetPropertyStoreForWindow + PKEY_AppUserModel_ID per window, falling back to GetApplicationUserModelId on the process for packaged apps, whose windows usually carry no explicit AUMID.
  • Resolve - WebAppService indexes .lnk files under the Start Menu and Desktop and keeps those carrying a Chromium app switch (--app-id= / --app=), so a window's AUMID maps back to the shortcut that launches it.
  • Restore - web apps launch through their shortcut (with a chrome.exe --app-id=<id> fallback), Store apps through shell:AppsFolder\<AUMID>.
  • Match - dedicated tiers match web apps by AUMID and single-site windows by URL host; IdentityCompatible stops a browser entry from claiming a PWA window, or the reverse. Where several windows of one app remain ambiguous, they are ranked by title similarity (Sorensen-Dice over character bigrams), which is robust against volatile parts of a title such as a live price.

Snapshots saved before this change carry no AUMID and keep the previous behaviour.

Also included

  • "Align + minimize others" restore mode - repositions a workspace's windows and minimizes everything else without closing anything; a non-destructive middle ground between Restore and Switch. Available per workspace from the tray menu.
  • Dedicated browser windows by URL - opt-in via dedicatedBrowserUrlPatterns in settings.json (a bare domain matches the whole site). Matching windows have their address-bar URL read via UI Automation and are reopened with --new-window <url>. When no patterns are configured, no URLs are read and snapshots are unchanged.
  • Bulk delete for saved workspaces - each row in Settings gets a checkbox plus a select-all box in the header; deleting a selection takes one confirmation instead of one per workspace.
  • Friendly names in the Save dialog - installed web apps show as "Insilico Terminal" rather than "brave".
  • Case-insensitive settings parsing - hand-edited keys apply instead of being silently ignored.

Changes

File Change
Native/NativeMethods.Shell.cs new - IPropertyStore, PROPVARIANT, IShellLinkW, IPersistFile, GetApplicationUserModelId
Services/WebAppService.cs new - AUMID lookup (window + process), shortcut index, app-id heuristics
Services/BrowserUrlService.cs new - address-bar URL via UI Automation, with timeout
Models/WindowRecord.cs, Models/WorkspaceEntry.cs AUMID, web-app and dedicated-browser fields
Models/AppSettings.cs DedicatedBrowserUrlPatterns
Services/WindowService.cs capture AUMID and URL; MinimizeUserWindowsExcept
Services/WorkspaceService.cs entry builders, launch paths, AUMID/URL/title matching
Services/SettingsService.cs case-insensitive JSON property matching
Services/LayoutCoordinator.cs, App.xaml.cs align mode, tray entry, service wiring
UI/SettingsWindow.xaml(.cs), UI/SaveWorkspaceDialog.xaml.cs bulk selection, display names

Note on CONTRIBUTING.md: the new P/Invoke declarations went into a third file, Native/NativeMethods.Shell.cs, rather than the two listed there - the shell property system and IShellLink COM interfaces did not fit either the Window or Display file. Happy to merge them elsewhere if preferred.

No new NuGet dependencies (System.Windows.Automation ships with the Windows Desktop SDK).

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change
  • Documentation update

Testing

Verified on Windows 11, two monitors, with a workspace containing two installed web apps (Insilico Terminal via Brave, aggr.trade via Chrome), two TradingView charts with different layouts, a single-site Brave window, ordinary Chrome and Brave windows, Store-installed Notepad, Explorer and Discord.

  • Saving logs one line per special window, e.g. [WebApp] 'Insilico Terminal' detected (AUMID=Brave._crx_..., source=WEB_APP_SHORTCUT); plain browser windows continue through the normal path.

  • Restoring a closed workspace brings both web apps back as app windows, TradingView back with its own dark theme, and the single-site window back at its URL alongside the normal multi-tab window.

  • Restoring with those windows already open repositions them instead of launching duplicates.

  • Two TradingView charts return to their own monitors rather than swapping.

  • Selecting several workspaces and deleting them removes exactly those, and the list refreshes with the selection cleared.

  • Tested on Windows 11

  • Tested on Windows 10

  • Tested with multiple monitors

  • Tested workspace save/restore

  • Tested file detection

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works

Known limitations

  • dedicatedBrowserUrlPatterns has no settings UI yet and must be added to settings.json by hand.
  • Where no shortcut exists for a web app, the launch command is rebuilt as --app-id=<id> without --profile-directory, so it opens in the default profile. Logged as a warning.
  • Disambiguating two windows of the same app relies on their titles differing; identical titles remain ambiguous.

…ity, disambiguate duplicate windows by title
Each workspace row gets a checkbox plus a select-all box in the header.
Ticking rows reveals a bar to delete the whole selection after one
confirmation, instead of removing workspaces one at a time.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant