Compare commits

..

3 Commits

Author SHA1 Message Date
Gordon Lam (SH)
c25e1d4ffa feat(winmd-api-search): add WinMD cache generator script and related files 2026-02-14 17:55:59 +08:00
Gordon Lam (SH)
7e2556fa3b docs(skills/pr-fix): add batch processing guidance with PowerShell parallel 2026-02-14 17:51:57 +08:00
Gordon Lam (SH)
af4007feea docs(skills): add critical guidance for parallel execution and PR quality
- Add PowerShell 7 ForEach-Object -Parallel pattern for single terminal execution
- Document that spawning multiple terminals is NOT trackable by the agent
- Add PR quality requirements (no stub code, proper titles, full descriptions)
- Add quality gate checks for worktree validation
- Document the correct copilot CLI invocation pattern
2026-02-14 17:51:57 +08:00
47 changed files with 1977 additions and 756 deletions

156
.github/skills/winmd-api-search/SKILL.md vendored Normal file
View File

@@ -0,0 +1,156 @@
---
name: winmd-api-search
description: 'Find and explore Windows desktop APIs. Use when building features that need platform capabilities — camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc. Discovers the right API for a task and retrieves full type details (methods, properties, events, enumeration values).'
---
# WinMD API Search
This skill helps you find the right Windows API for any capability and get its full details. It searches a local cache of all WinMD metadata from:
- **Windows Platform SDK** — all `Windows.*` WinRT APIs (always available, no restore needed)
- **WinAppSDK / WinUI** — bundled as a baseline in the cache generator (always available, no restore needed)
- **NuGet packages** — any additional packages in restored projects that contain `.winmd` files
- **Project-output WinMD** — class libraries (C++/WinRT, C#) that produce `.winmd` as build output
Even on a fresh clone with no restore or build, you still get full Platform SDK + WinAppSDK coverage.
## When to Use
- User wants to build a feature and you need to find which API provides that capability
- User asks "how do I do X?" where X involves a platform feature (camera, files, notifications, sensors, AI, etc.)
- You need the exact methods, properties, events, or enumeration values of a type before writing code
- You're unsure which control, class, or interface to use for a UI or system task
## How to Use
Pick the path that matches the situation:
---
### Discover — "I don't know which API to use"
The user describes a capability in their own words. You need to find the right API.
**1. Translate user language → search keywords**
Map the user's daily language to programming terms. Try multiple variations:
| User says | Search keywords to try (in order) |
|-----------|-----------------------------------|
| "take a picture" | `camera`, `capture`, `photo`, `MediaCapture` |
| "load from disk" | `file open`, `picker`, `FileOpen`, `StorageFile` |
| "describe what's in it" | `image description`, `Vision`, `Recognition` |
| "show a popup" | `dialog`, `flyout`, `popup`, `ContentDialog` |
| "drag and drop" | `drag`, `drop`, `DragDrop` |
| "save settings" | `settings`, `ApplicationData`, `LocalSettings` |
Start with simple everyday words. If results are weak or irrelevant, try the more technical variation.
**2. Run searches**
```powershell
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action search -Query "<keyword>"
```
This returns ranked namespaces with top matching types and the **JSON file path**.
**3. Read the JSON to choose the right API**
Use `read_file` on the file path(s) from the top results. The JSON has all types in that namespace — full members, signatures, parameters, return types, enumeration values.
Read and decide which types and members fit the user's requirement.
**4. Use the API knowledge to answer or write code**
---
### Lookup — "I know the API, show me the details"
You already know (or suspect) the type or namespace name. Go direct:
```powershell
# Get all members of a known type
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.NavigationView"
# Get enum values
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility"
# List all types in a namespace
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls"
# Browse namespaces
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI"
```
If you need full detail beyond what `-Action members` shows, use `-Action search` to get the JSON file path, then `read_file` the JSON directly.
---
### Other Commands
```powershell
# List cached projects
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action projects
# Show stats
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action stats
```
> If only one project is cached, `-Project` is auto-selected.
> If multiple projects exist, add `-Project <name>`.
## Search Scoring
The search ranks type names against your query:
| Score | Match type | Example |
|-------|-----------|---------|
| 100 | Exact name | `Button``Button` |
| 80 | Starts with | `Navigation``NavigationView` |
| 60 | Contains | `Dialog``ContentDialog` |
| 50 | PascalCase initials | `ASB``AutoSuggestBox` |
| 40 | Multi-keyword AND | `navigation item``NavigationViewItem` |
| 20 | Fuzzy character match | `NavVw``NavigationView` |
Results are grouped by namespace. Higher-scored namespaces appear first.
## Cache Setup
If the cache doesn't exist, generate it:
```powershell
# Single project
.\.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1 -ProjectDir <project-folder>
# All projects in the repo
.\.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1
```
Requires:
- **.NET SDK 8.0 or later** — auto-detects the highest installed SDK (>= 8). Install from [dotnet.microsoft.com](https://dotnet.microsoft.com/download) if not available.
No project restore or build is needed for baseline coverage (Platform SDK + WinAppSDK). For additional NuGet packages, the project needs `dotnet restore` (which generates `project.assets.json`) or a `packages.config` file.
Cache is stored at `Generated Files\winmd-cache\`, deduplicated per-package+version.
### What gets indexed
| Source | When available |
|--------|----------------|
| Windows Platform SDK | Always (reads from local SDK install) |
| WinAppSDK (latest) | Always (bundled as baseline in cache generator) |
| Project NuGet packages | After `dotnet restore` or with `packages.config` |
| Project-output `.winmd` | After project build (class libraries that produce WinMD) |
> **Note:** This cache directory should be in `.gitignore` — it's generated, not source.
## Troubleshooting
| Issue | Fix |
|-------|-----|
| "Cache not found" | Run `Update-WinMdCache.ps1` |
| "Multiple projects cached" | Add `-Project <name>` |
| "Namespace not found" | Use `-Action namespaces` to list available ones |
| "Type not found" | Use fully qualified name (e.g., `Microsoft.UI.Xaml.Controls.Button`) |
| Stale after NuGet update | Re-run `Update-WinMdCache.ps1` |
| Cache in git history | Add `Generated Files/` to `.gitignore` |

View File

@@ -0,0 +1,455 @@
<#
.SYNOPSIS
Query WinMD API metadata from cached JSON files.
.DESCRIPTION
Reads pre-built JSON cache of WinMD types, members, and namespaces.
The cache is organized per-package (deduplicated) with project manifests
that map each project to its referenced packages.
Supports listing namespaces, types, members, searching, enum value lookup,
and listing cached projects/packages.
.PARAMETER Action
The query action to perform:
- projects : List cached projects
- packages : List packages for a project
- stats : Show aggregate statistics for a project
- namespaces : List all namespaces (optional -Filter prefix)
- types : List types in a namespace (-Namespace required)
- members : List members of a type (-TypeName required)
- search : Search types and members (-Query required)
- enums : List enum values (-TypeName required)
.PARAMETER Project
Project name to query. Auto-selected if only one project is cached.
Use -Action projects to list available projects.
.PARAMETER Namespace
Namespace to query types from (used with -Action types).
.PARAMETER TypeName
Full type name e.g. "Microsoft.UI.Xaml.Controls.Button" (used with -Action members, enums).
.PARAMETER Query
Search query string (used with -Action search).
.PARAMETER Filter
Optional prefix filter for namespaces (used with -Action namespaces).
.PARAMETER CacheDir
Path to the winmd-cache directory. Defaults to "Generated Files\winmd-cache"
relative to the workspace root.
.PARAMETER MaxResults
Maximum number of results to return for search. Defaults to 30.
.EXAMPLE
.\Invoke-WinMdQuery.ps1 -Action projects
.\Invoke-WinMdQuery.ps1 -Action packages -Project BlankWInUI
.\Invoke-WinMdQuery.ps1 -Action stats -Project BlankWInUI
.\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI"
.\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls"
.\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.Button"
.\Invoke-WinMdQuery.ps1 -Action search -Query "NavigationView"
.\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateSet('projects', 'packages', 'stats', 'namespaces', 'types', 'members', 'search', 'enums')]
[string]$Action,
[string]$Project,
[string]$Namespace,
[string]$TypeName,
[string]$Query,
[string]$Filter,
[string]$CacheDir,
[int]$MaxResults = 30
)
# ─── Resolve cache directory ─────────────────────────────────────────────────
if (-not $CacheDir) {
# Convention: skill lives at .github/skills/winmd-api-search/scripts/
# so workspace root is 4 levels up from $PSScriptRoot.
$scriptDir = $PSScriptRoot
$root = (Resolve-Path (Join-Path $scriptDir '..\..\..\..')).Path
$CacheDir = Join-Path $root 'Generated Files\winmd-cache'
}
if (-not (Test-Path $CacheDir)) {
Write-Error "Cache not found at: $CacheDir`nRun: .\Update-WinMdCache.ps1 (from .github\skills\winmd-api-search\scripts\)"
exit 1
}
# ─── Project resolution helpers ──────────────────────────────────────────────
function Get-CachedProjects {
$projectsDir = Join-Path $CacheDir 'projects'
if (-not (Test-Path $projectsDir)) { return @() }
Get-ChildItem $projectsDir -Filter '*.json' | ForEach-Object { $_.BaseName }
}
function Resolve-ProjectManifest {
param([string]$Name)
$projectsDir = Join-Path $CacheDir 'projects'
if (-not (Test-Path $projectsDir)) {
Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
exit 1
}
if ($Name) {
$path = Join-Path $projectsDir "$Name.json"
if (-not (Test-Path $path)) {
$available = (Get-CachedProjects) -join ', '
Write-Error "Project '$Name' not found. Available: $available"
exit 1
}
return Get-Content $path | ConvertFrom-Json
}
# Auto-select if only one project
$manifests = Get-ChildItem $projectsDir -Filter '*.json' -ErrorAction SilentlyContinue
if ($manifests.Count -eq 0) {
Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
exit 1
}
if ($manifests.Count -eq 1) {
return Get-Content $manifests[0].FullName | ConvertFrom-Json
}
$available = ($manifests | ForEach-Object { $_.BaseName }) -join ', '
Write-Error "Multiple projects cached — use -Project to specify. Available: $available"
exit 1
}
function Get-PackageCacheDirs {
param($Manifest)
$dirs = @()
foreach ($pkg in $Manifest.packages) {
$dir = Join-Path $CacheDir 'packages' $pkg.id $pkg.version
if (Test-Path $dir) {
$dirs += $dir
}
}
return $dirs
}
# ─── Action: projects ────────────────────────────────────────────────────────
function Show-Projects {
$projects = Get-CachedProjects
if ($projects.Count -eq 0) {
Write-Output "No projects cached."
return
}
Write-Output "Cached projects ($($projects.Count)):"
foreach ($p in $projects) {
$manifest = Get-Content (Join-Path $CacheDir 'projects' "$p.json") | ConvertFrom-Json
$pkgCount = $manifest.packages.Count
Write-Output " $p ($pkgCount package(s))"
}
}
# ─── Action: packages ────────────────────────────────────────────────────────
function Show-Packages {
$manifest = Resolve-ProjectManifest -Name $Project
Write-Output "Packages for project '$($manifest.projectName)' ($($manifest.packages.Count)):"
foreach ($pkg in $manifest.packages) {
$metaPath = Join-Path $CacheDir 'packages' $pkg.id $pkg.version 'meta.json'
if (Test-Path $metaPath) {
$meta = Get-Content $metaPath | ConvertFrom-Json
Write-Output " $($pkg.id)@$($pkg.version)$($meta.totalTypes) types, $($meta.totalMembers) members"
} else {
Write-Output " $($pkg.id)@$($pkg.version) — (cache missing)"
}
}
}
# ─── Action: stats ───────────────────────────────────────────────────────────
function Show-Stats {
$manifest = Resolve-ProjectManifest -Name $Project
$totalTypes = 0
$totalMembers = 0
$totalNamespaces = 0
$totalWinMd = 0
foreach ($pkg in $manifest.packages) {
$metaPath = Join-Path $CacheDir 'packages' $pkg.id $pkg.version 'meta.json'
if (Test-Path $metaPath) {
$meta = Get-Content $metaPath | ConvertFrom-Json
$totalTypes += $meta.totalTypes
$totalMembers += $meta.totalMembers
$totalNamespaces += $meta.totalNamespaces
$totalWinMd += $meta.winMdFiles.Count
}
}
Write-Output "WinMD Index Statistics — $($manifest.projectName)"
Write-Output "======================================"
Write-Output " Packages: $($manifest.packages.Count)"
Write-Output " Namespaces: $totalNamespaces (may overlap across packages)"
Write-Output " Types: $totalTypes"
Write-Output " Members: $totalMembers"
Write-Output " WinMD files: $totalWinMd"
}
# ─── Action: namespaces ──────────────────────────────────────────────────────
function Get-Namespaces {
param([string]$Prefix)
$manifest = Resolve-ProjectManifest -Name $Project
$dirs = Get-PackageCacheDirs -Manifest $manifest
$allNs = @()
foreach ($dir in $dirs) {
$nsFile = Join-Path $dir 'namespaces.json'
if (Test-Path $nsFile) {
$allNs += (Get-Content $nsFile | ConvertFrom-Json)
}
}
$allNs = $allNs | Sort-Object -Unique
if ($Prefix) {
$allNs = $allNs | Where-Object { $_ -like "$Prefix*" }
}
$allNs | ForEach-Object { Write-Output $_ }
}
# ─── Action: types ───────────────────────────────────────────────────────────
function Get-TypesInNamespace {
param([string]$Ns)
if (-not $Ns) {
Write-Error "-Namespace is required for 'types' action."
exit 1
}
$manifest = Resolve-ProjectManifest -Name $Project
$dirs = Get-PackageCacheDirs -Manifest $manifest
$safeFile = $Ns.Replace('.', '_') + '.json'
$found = $false
$seen = @{}
foreach ($dir in $dirs) {
$filePath = Join-Path $dir "types\$safeFile"
if (-not (Test-Path $filePath)) { continue }
$found = $true
$types = Get-Content $filePath | ConvertFrom-Json
foreach ($t in $types) {
if ($seen.ContainsKey($t.fullName)) { continue }
$seen[$t.fullName] = $true
Write-Output "$($t.kind) $($t.fullName)$(if ($t.baseType) { " : $($t.baseType)" } else { '' })"
}
}
if (-not $found) {
Write-Error "Namespace not found: $Ns"
exit 1
}
}
# ─── Action: members ─────────────────────────────────────────────────────────
function Get-MembersOfType {
param([string]$FullName)
if (-not $FullName) {
Write-Error "-TypeName is required for 'members' action."
exit 1
}
$ns = $FullName.Substring(0, $FullName.LastIndexOf('.'))
$safeFile = $ns.Replace('.', '_') + '.json'
$manifest = Resolve-ProjectManifest -Name $Project
$dirs = Get-PackageCacheDirs -Manifest $manifest
foreach ($dir in $dirs) {
$filePath = Join-Path $dir "types\$safeFile"
if (-not (Test-Path $filePath)) { continue }
$types = Get-Content $filePath | ConvertFrom-Json
$type = $types | Where-Object { $_.fullName -eq $FullName }
if (-not $type) { continue }
Write-Output "$($type.kind) $($type.fullName)"
if ($type.baseType) { Write-Output " Extends: $($type.baseType)" }
Write-Output ""
foreach ($m in $type.members) {
Write-Output " [$($m.kind)] $($m.signature)"
}
return
}
Write-Error "Type not found: $FullName"
exit 1
}
# ─── Action: search ──────────────────────────────────────────────────────────
# Ranks namespaces by best type-name match score.
# Outputs: ranked namespaces with top matching types and the JSON file path.
# The LLM can then read_file the JSON to inspect all members intelligently.
function Search-WinMd {
param([string]$SearchQuery, [int]$Max)
if (-not $SearchQuery) {
Write-Error "-Query is required for 'search' action."
exit 1
}
$manifest = Resolve-ProjectManifest -Name $Project
$dirs = Get-PackageCacheDirs -Manifest $manifest
# Collect: namespace → { bestScore, matchingTypes[], filePath }
$nsResults = @{}
foreach ($dir in $dirs) {
$nsFile = Join-Path $dir 'namespaces.json'
if (-not (Test-Path $nsFile)) { continue }
$nsList = Get-Content $nsFile | ConvertFrom-Json
foreach ($n in $nsList) {
$safeFile = $n.Replace('.', '_') + '.json'
$filePath = Join-Path $dir "types\$safeFile"
if (-not (Test-Path $filePath)) { continue }
$types = Get-Content $filePath | ConvertFrom-Json
foreach ($t in $types) {
$score = Get-MatchScore -Name $t.name -FullName $t.fullName -Query $SearchQuery
if ($score -le 0) { continue }
if (-not $nsResults.ContainsKey($n)) {
$nsResults[$n] = @{ BestScore = 0; Types = @(); FilePath = $filePath }
}
$entry = $nsResults[$n]
if ($score -gt $entry.BestScore) { $entry.BestScore = $score }
$entry.Types += "$($t.kind) $($t.fullName) [$score]"
}
}
}
if ($nsResults.Count -eq 0) {
Write-Output "No results found for: $SearchQuery"
return
}
$ranked = $nsResults.GetEnumerator() |
Sort-Object { $_.Value.BestScore } -Descending |
Select-Object -First $Max
foreach ($r in $ranked) {
$ns = $r.Key
$info = $r.Value
Write-Output "[$($info.BestScore)] $ns"
Write-Output " File: $($info.FilePath)"
# Show top 5 matching types in this namespace
$info.Types | Select-Object -First 5 | ForEach-Object { Write-Output " $_" }
Write-Output ""
}
}
# ─── Search scoring ──────────────────────────────────────────────────────────
# Simple ranked scoring on type names. Higher = better.
# 100 = exact name 80 = starts-with 60 = substring
# 50 = PascalCase 40 = multi-keyword 20 = fuzzy subsequence
function Get-MatchScore {
param([string]$Name, [string]$FullName, [string]$Query)
$q = $Query.Trim()
if (-not $q) { return 0 }
if ($Name -eq $q) { return 100 }
if ($Name -like "$q*") { return 80 }
if ($Name -like "*$q*" -or $FullName -like "*$q*") { return 60 }
$initials = ($Name.ToCharArray() | Where-Object { [char]::IsUpper($_) }) -join ''
if ($initials.Length -ge 2 -and $initials -like "*$q*") { return 50 }
$words = $q -split '\s+' | Where-Object { $_.Length -gt 0 }
if ($words.Count -gt 1) {
$allFound = $true
foreach ($w in $words) {
if ($Name -notlike "*$w*" -and $FullName -notlike "*$w*") {
$allFound = $false
break
}
}
if ($allFound) { return 40 }
}
if (Test-FuzzySubsequence -Text $Name -Pattern $q) { return 20 }
return 0
}
function Test-FuzzySubsequence {
param([string]$Text, [string]$Pattern)
$ti = 0
$tLower = $Text.ToLowerInvariant()
$pLower = $Pattern.ToLowerInvariant()
foreach ($ch in $pLower.ToCharArray()) {
$idx = $tLower.IndexOf($ch, $ti)
if ($idx -lt 0) { return $false }
$ti = $idx + 1
}
return $true
}
# ─── Action: enums ───────────────────────────────────────────────────────────
function Get-EnumValues {
param([string]$FullName)
if (-not $FullName) {
Write-Error "-TypeName is required for 'enums' action."
exit 1
}
$ns = $FullName.Substring(0, $FullName.LastIndexOf('.'))
$safeFile = $ns.Replace('.', '_') + '.json'
$manifest = Resolve-ProjectManifest -Name $Project
$dirs = Get-PackageCacheDirs -Manifest $manifest
foreach ($dir in $dirs) {
$filePath = Join-Path $dir "types\$safeFile"
if (-not (Test-Path $filePath)) { continue }
$types = Get-Content $filePath | ConvertFrom-Json
$type = $types | Where-Object { $_.fullName -eq $FullName }
if (-not $type) { continue }
if ($type.kind -ne 'Enum') {
Write-Error "$FullName is not an Enum (kind: $($type.kind))"
exit 1
}
Write-Output "Enum $($type.fullName)"
if ($type.enumValues) {
$type.enumValues | ForEach-Object { Write-Output " $_" }
} else {
Write-Output " (no values)"
}
return
}
Write-Error "Type not found: $FullName"
exit 1
}
# ─── Dispatch ─────────────────────────────────────────────────────────────────
switch ($Action) {
'projects' { Show-Projects }
'packages' { Show-Packages }
'stats' { Show-Stats }
'namespaces' { Get-Namespaces -Prefix $Filter }
'types' { Get-TypesInNamespace -Ns $Namespace }
'members' { Get-MembersOfType -FullName $TypeName }
'search' { Search-WinMd -SearchQuery $Query -Max $MaxResults }
'enums' { Get-EnumValues -FullName $TypeName }
}

View File

@@ -0,0 +1,113 @@
<#
.SYNOPSIS
Generate or refresh the WinMD cache for the Agent Skill.
.DESCRIPTION
Builds and runs the standalone cache generator to export cached JSON files
from all WinMD metadata found in project NuGet packages and Windows SDK.
The cache is per-package+version: if two projects reference the same
package at the same version, the WinMD data is parsed once and shared.
Supports single project or recursive scan of an entire repo.
.PARAMETER ProjectDir
Path to a project directory (contains .csproj/.vcxproj), or a project file itself.
Defaults to scanning the workspace root.
.PARAMETER Scan
Recursively discover all .csproj/.vcxproj files under ProjectDir.
.PARAMETER OutputDir
Path to the cache output directory. Defaults to "Generated Files\winmd-cache".
.EXAMPLE
.\Update-WinMdCache.ps1
.\Update-WinMdCache.ps1 -ProjectDir BlankWInUI
.\Update-WinMdCache.ps1 -Scan -ProjectDir .
.\Update-WinMdCache.ps1 -ProjectDir "src\MyApp\MyApp.csproj"
#>
[CmdletBinding()]
param(
[string]$ProjectDir,
[switch]$Scan,
[string]$OutputDir = 'Generated Files\winmd-cache'
)
$ErrorActionPreference = 'Stop'
# Convention: skill lives at .github/skills/winmd-api-search/scripts/
# so workspace root is 4 levels up from $PSScriptRoot.
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')).Path
$generatorProj = Join-Path $PSScriptRoot 'cache-generator' 'CacheGenerator.csproj'
# Default: if no ProjectDir, scan the workspace root
if (-not $ProjectDir) {
$ProjectDir = $root
$Scan = $true
}
Push-Location $root
try {
# Detect installed .NET SDK — require >= 8.0
$dotnetSdks = dotnet --list-sdks 2>$null
$bestMajor = $dotnetSdks |
ForEach-Object { if ($_ -match '^(\d+)\.') { [int]$Matches[1] } } |
Where-Object { $_ -ge 8 } |
Sort-Object -Descending |
Select-Object -First 1
if (-not $bestMajor) {
Write-Error "No .NET SDK >= 8.0 found. Install from https://dotnet.microsoft.com/download"
exit 1
}
$targetFramework = "net$bestMajor.0"
Write-Host "Using .NET SDK: $targetFramework" -ForegroundColor Cyan
Write-Host "Building cache generator..." -ForegroundColor Cyan
dotnet restore $generatorProj -p:TargetFramework=$targetFramework --nologo -v q
dotnet build $generatorProj -c Release --nologo -v q -p:TargetFramework=$targetFramework --no-restore
if ($LASTEXITCODE -ne 0) {
Write-Error "Build failed"
exit 1
}
# Run the built executable directly (avoids dotnet run target framework mismatch issues)
$generatorDir = Join-Path $PSScriptRoot 'cache-generator'
$exePath = Join-Path $generatorDir "bin\Release\$targetFramework\CacheGenerator.exe"
if (-not (Test-Path $exePath)) {
# Fallback: try dll with dotnet
$dllPath = Join-Path $generatorDir "bin\Release\$targetFramework\CacheGenerator.dll"
if (Test-Path $dllPath) {
$exePath = $null
} else {
Write-Error "Built executable not found at: $exePath"
exit 1
}
}
$runArgs = @()
if ($Scan) {
$runArgs += '--scan'
}
$runArgs += $ProjectDir
$runArgs += $OutputDir
Write-Host "Exporting WinMD cache..." -ForegroundColor Cyan
if ($exePath) {
& $exePath @runArgs
} else {
dotnet $dllPath @runArgs
}
if ($LASTEXITCODE -ne 0) {
Write-Error "Cache export failed"
exit 1
}
Write-Host "Cache updated at: $OutputDir" -ForegroundColor Green
} finally {
Pop-Location
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<!-- Default fallback; Update-WinMdCache.ps1 overrides via -p:TargetFramework=net{X}.0 -->
<TargetFramework Condition="'$(TargetFramework)' == ''">net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<!-- System.Reflection.Metadata is inbox in net9.0+, only needed for net8.0 -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="System.Reflection.Metadata" Version="9.*" />
</ItemGroup>
<!--
Baseline WinAppSDK packages: downloaded during restore so the cache generator
can always index WinAppSDK APIs, even if the target project hasn't been restored.
ExcludeAssets="all" means they're downloaded but don't affect this tool's build.
-->
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="*" ExcludeAssets="all" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,3 @@
<Project>
<!-- Isolate this standalone tool from the repo-level build configuration -->
</Project>

View File

@@ -0,0 +1,3 @@
<Project>
<!-- Isolate this standalone tool from the repo-level build targets -->
</Project>

View File

@@ -0,0 +1,3 @@
<Project>
<!-- Isolate this standalone tool from the repo-level Central Package Management -->
</Project>

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,6 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider
"env",
"environment",
"manifest",
"log",
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
public IEnumerable<SanitizationRule> GetRules()
@@ -62,11 +61,6 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider
return full;
}
if (IsVersionSegment(file))
{
return full;
}
string stem, ext;
if (dot > 0 && dot < file.Length - 1)
{
@@ -112,30 +106,4 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider
var maskedCount = Math.Max(1, stem.Length - keep);
return stem[..keep] + new string('*', maskedCount);
}
private static bool IsVersionSegment(string file)
{
var dotIndex = file.IndexOf('.');
if (dotIndex <= 0 || dotIndex == file.Length - 1)
{
return false;
}
var hasDot = false;
foreach (var ch in file)
{
if (ch == '.')
{
hasDot = true;
continue;
}
if (!char.IsDigit(ch))
{
return false;
}
}
return hasDot;
}
}

View File

@@ -11,9 +11,7 @@ internal sealed partial class NetworkRuleProvider : ISanitizationRuleProvider
{
public IEnumerable<SanitizationRule> GetRules()
{
// Disabled for now as these rules can be too aggressive and cause over-sanitization, especially in scenarios like
// error report sanitization where we want to preserve as much useful information as possible while still protecting sensitive data.
// yield return new(Ipv4Rx(), "[IP4_REDACTED]", "IP addresses");
yield return new(Ipv4Rx(), "[IP4_REDACTED]", "IP addresses");
yield return new(Ipv6BracketedRx(), "[IP6_REDACTED]", "IPv6 addresses (bracketed/with port)");
yield return new(Ipv6Rx(), "[IP6_REDACTED]", "IPv6 addresses");
yield return new(MacAddressRx(), "[MAC_ADDRESS_REDACTED]", "MAC addresses");

View File

@@ -25,56 +25,52 @@ internal sealed partial class PiiRuleProvider : ISanitizationRuleProvider
private static partial Regex EmailRx();
[GeneratedRegex("""
(?xi)
# ---------- boundaries ----------
(?<!\w) # not after a letter/digit/underscore
(?<![A-Za-z0-9]-) # avoid starting inside hyphenated tokens (GUID middles, etc.)
(?xi)
# ---------- boundaries ----------
(?<!\w) # not after a letter/digit/underscore
(?<![A-Za-z0-9]-) # avoid starting inside hyphenated tokens (GUID middles, etc.)
# ---------- global do-not-match guards ----------
(?! # ISO date (yyyy-mm-dd / yyyy.mm.dd / yyyy/mm/dd)
(?:19|20)\d{2}[-./](?:0[1-9]|1[0-2])[-./](?:0[1-9]|[12]\d|3[01])\b
)
(?! # EU date (dd-mm-yyyy / dd.mm.yyyy / dd/mm/yyyy)
(?:0[1-9]|[12]\d|3[01])[-./](?:0[1-9]|1[0-2])[-./](?:19|20)\d{2}\b
)
(?! # ISO datetime like 2025-08-24T14:32[:ss][Z|±hh:mm]
(?:19|20)\d{2}-\d{2}-\d{2}[T\s]\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?\b
)
(?!\b(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?\b) # IPv4 with optional :port
(?!\b[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}\b) # GUID, lowercase
(?!\b[0-9A-F]{8}-(?:[0-9A-F]{4}-){3}[0-9A-F]{12}\b) # GUID, uppercase
(?!\bv?\d+(?:\.\d+){2,}\b) # semantic/file versions like 1.2.3 or 10.0.22631.3448
(?!\b(?:[0-9A-F]{2}[:-]){5}[0-9A-F]{2}\b) # MAC address
# ---------- global do-not-match guards ----------
(?! # ISO date (yyyy-mm-dd / yyyy.mm.dd / yyyy/mm/dd)
(?:19|20)\d{2}[-./](?:0[1-9]|1[0-2])[-./](?:0[1-9]|[12]\d|3[01])\b
)
(?! # EU date (dd-mm-yyyy / dd.mm.yyyy / dd/mm/yyyy)
(?:0[1-9]|[12]\d|3[01])[-./](?:0[1-9]|1[0-2])[-./](?:19|20)\d{2}\b
)
(?! # ISO datetime like 2025-08-24T14:32[:ss][Z|±hh:mm]
(?:19|20)\d{2}-\d{2}-\d{2}[T\s]\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?\b
)
(?!\b(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?\b) # IPv4 with optional :port
(?!\b[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}\b) # GUID, lowercase
(?!\b[0-9A-F]{8}-(?:[0-9A-F]{4}-){3}[0-9A-F]{12}\b) # GUID, uppercase
(?!\bv?\d+(?:\.\d+){2,}\b) # semantic/file versions like 1.2.3 or 10.0.22631.3448
(?!\b(?:[0-9A-F]{2}[:-]){5}[0-9A-F]{2}\b) # MAC address
# ---------- digit budget ----------
(?=(?:[^\r\n]*\d){7,15}[^\r\n]*(?:\r\n|$))
(?=(?:\D*\d){7,15}) # 715 digits in total
# ---------- digit budget ----------
(?=(?:\D*\d){7,15}) # 715 digits in total
# ---------- number body ----------
(?:
# A with explicit country code, allow compact digits (E.164-ish) or grouped
(?:\+|00)[1-9]\d{0,2}
(?:
[\p{Zs}.\-\/]*\d{6,14}
|
[\p{Zs}.\-\/]* (?:\(\d{1,4}\)|\d{1,4})
(?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6}
)
|
# ---------- number body ----------
(?:
# A with explicit country code, allow compact digits (E.164-ish) or grouped
(?:\+|00)[1-9]\d{0,2}
(?:
[\p{Zs}.\-\/]*\d{6,14}
|
[\p{Zs}.\-\/]* (?:\(\d{1,4}\)|\d{1,4})
(?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6}
)
|
# B no country code => require separators between blocks (avoid plain big ints)
(?:\(\d{1,4}\)|\d{1,4})
(?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6}
)
(?:\(\d{1,4}\)|\d{1,4})
(?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6}
)
# ---------- optional extension ----------
(?:[\p{Zs}.\-,:;]* (?:ext\.?|x) [\p{Zs}]* (?<ext>\d{1,6}))?
# ---------- optional extension ----------
(?:[\p{Zs}.\-,:;]* (?:ext\.?|x) [\p{Zs}]* (?<ext>\d{1,6}))?
# ---------- end boundary (allow whitespace/newlines at edges) ----------
(?!-\w) # don't end just before '-letter'/'-digit'
(?!\w) # don't be immediately followed by a word char
""",
SanitizerDefaults.DefaultOptions | RegexOptions.IgnorePatternWhitespace,
SanitizerDefaults.DefaultMatchTimeoutMs)]
(?!-\w) # don't end just before '-letter'/'-digit'
""",
SanitizerDefaults.DefaultOptions | RegexOptions.IgnorePatternWhitespace, SanitizerDefaults.DefaultMatchTimeoutMs)]
private static partial Regex PhoneRx();
[GeneratedRegex(@"\b\d{3}-\d{2}-\d{4}\b",

View File

@@ -165,6 +165,4 @@ public interface IAppHostService
AppExtensionHost GetDefaultHost();
AppExtensionHost GetHostForCommand(object? context, AppExtensionHost? currentHost);
CommandProviderContext GetProviderContextForCommand(object? command, CommandProviderContext? currentContext);
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels;
public sealed class CommandProviderContext
{
public required string ProviderId { get; init; }
public static CommandProviderContext Empty { get; } = new() { ProviderId = "<EMPTY>" };
}

View File

@@ -47,8 +47,8 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext)
: base(model, scheduler, host, providerContext)
public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host)
: base(model, scheduler, host)
{
_model = new(model);
}

View File

@@ -89,8 +89,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
}
}
public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext)
: base(model, scheduler, host, providerContext)
public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host)
: base(model, scheduler, host)
{
_model = new(model);
EmptyContent = new(new(null), PageContext);

View File

@@ -9,7 +9,7 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public partial class LoadingPageViewModel : PageViewModel
{
public LoadingPageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost host)
: base(model, scheduler, host, CommandProviderContext.Empty)
: base(model, scheduler, host)
{
ModelIsLoading = true;
IsInitialized = false;

View File

@@ -5,4 +5,4 @@
namespace Microsoft.CmdPal.Core.ViewModels;
internal sealed partial class NullPageViewModel(TaskScheduler scheduler, AppExtensionHost extensionHost)
: PageViewModel(null, scheduler, extensionHost, CommandProviderContext.Empty);
: PageViewModel(null, scheduler, extensionHost);

View File

@@ -76,16 +76,13 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
public IconInfoViewModel Icon { get; protected set; }
public CommandProviderContext ProviderContext { get; protected set; }
public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost, CommandProviderContext providerContext)
public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost)
: base(scheduler)
{
InitializeSelfAsPageContext();
_pageModel = new(model);
Scheduler = scheduler;
ExtensionHost = extensionHost;
ProviderContext = providerContext;
Icon = new(null);
ExtensionHost.StatusMessages.CollectionChanged += StatusMessages_CollectionChanged;
@@ -278,5 +275,5 @@ public interface IPageViewModelFactoryService
/// <param name="nested">Indicates whether the page is not the top-level page.</param>
/// <param name="host">The command palette host that will host the page (for status messages)</param>
/// <returns>A new instance of the page view model.</returns>
PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext);
PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host);
}

View File

@@ -258,7 +258,6 @@ public partial class ShellViewModel : ObservableObject,
}
var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost);
var providerContext = _appHostService.GetProviderContextForCommand(message.Context, CurrentPage.ProviderContext);
_rootPageService.OnPerformCommand(message.Context, !CurrentPage.IsNested, host);
@@ -274,15 +273,15 @@ public partial class ShellViewModel : ObservableObject,
// Telemetry: Track extension page navigation for session metrics
if (host is not null)
{
var extensionId = host.GetExtensionDisplayName() ?? "builtin";
var commandId = command?.Id ?? "unknown";
var commandName = command?.Name ?? "unknown";
string extensionId = host.GetExtensionDisplayName() ?? "builtin";
string commandId = command?.Id ?? "unknown";
string commandName = command?.Name ?? "unknown";
WeakReferenceMessenger.Default.Send<TelemetryExtensionInvokedMessage>(
new(extensionId, commandId, commandName, true, 0));
}
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host!, providerContext);
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host!);
if (pageViewModel is null)
{
CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
@@ -353,10 +352,10 @@ public partial class ShellViewModel : ObservableObject,
// Telemetry: Track command execution time and success
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var command = message.Command.Unsafe;
var extensionId = host?.GetExtensionDisplayName() ?? "builtin";
var commandId = command?.Id ?? "unknown";
var commandName = command?.Name ?? "unknown";
var success = false;
string extensionId = host?.GetExtensionDisplayName() ?? "builtin";
string commandId = command?.Id ?? "unknown";
string commandName = command?.Name ?? "unknown";
bool success = false;
try
{

View File

@@ -9,8 +9,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandPaletteContentPageViewModel : ContentPageViewModel
{
public CommandPaletteContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext)
: base(model, scheduler, host, providerContext)
public CommandPaletteContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host)
: base(model, scheduler, host)
{
}

View File

@@ -17,12 +17,12 @@ public class CommandPalettePageViewModelFactory
_scheduler = scheduler;
}
public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext)
public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host)
{
return page switch
{
IListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext) { IsNested = nested },
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host, providerContext),
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested },
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host),
_ => null,
};
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -8,7 +8,6 @@ using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.DependencyInjection;
using Windows.Foundation;
@@ -159,9 +158,6 @@ public sealed class CommandProviderWrapper
UnsafePreCacheApiAdditions(two);
}
// Load pinned commands from saved settings
var pinnedCommands = LoadPinnedCommands(model, providerSettings);
Id = model.Id;
DisplayName = model.DisplayName;
Icon = new(model.Icon);
@@ -179,7 +175,7 @@ public sealed class CommandProviderWrapper
Settings = new(model.Settings, this, _taskScheduler);
// We do need to explicitly initialize commands though
InitializeCommands(commands, fallbacks, pinnedCommands, serviceProvider, pageContext);
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
}
@@ -210,34 +206,27 @@ public sealed class CommandProviderWrapper
}
}
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, ICommandItem[] pinnedCommands, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var providerSettings = GetProviderSettings(settings);
var ourContext = GetProviderContext();
var makeAndAdd = (ICommandItem? i, bool fallback) =>
{
CommandItemViewModel commandItemViewModel = new(new(i), pageContext);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i);
topLevelViewModel.InitializeProperties();
return topLevelViewModel;
};
var topLevelList = new List<TopLevelViewModel>();
if (commands is not null)
{
topLevelList.AddRange(commands.Select(c => makeAndAdd(c, false)));
TopLevelItems = commands
.Select(c => makeAndAdd(c, false))
.ToArray();
}
if (pinnedCommands is not null)
{
topLevelList.AddRange(pinnedCommands.Select(c => makeAndAdd(c, false)));
}
TopLevelItems = topLevelList.ToArray();
if (fallbacks is not null)
{
FallbackItems = fallbacks
@@ -246,32 +235,6 @@ public sealed class CommandProviderWrapper
}
}
private ICommandItem[] LoadPinnedCommands(ICommandProvider model, ProviderSettings providerSettings)
{
var pinnedItems = new List<ICommandItem>();
if (model is ICommandProvider4 provider4)
{
foreach (var pinnedId in providerSettings.PinnedCommandIds)
{
try
{
var commandItem = provider4.GetCommandItem(pinnedId);
if (commandItem is not null)
{
pinnedItems.Add(commandItem);
}
}
catch (Exception e)
{
Logger.LogError($"Failed to load pinned command {pinnedId}: {e.Message}");
}
}
}
return pinnedItems.ToArray();
}
private void UnsafePreCacheApiAdditions(ICommandProvider2 provider)
{
var apiExtensions = provider.GetApiExtensionStubs();
@@ -285,26 +248,6 @@ public sealed class CommandProviderWrapper
}
}
public void PinCommand(string commandId, IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var providerSettings = GetProviderSettings(settings);
if (!providerSettings.PinnedCommandIds.Contains(commandId))
{
providerSettings.PinnedCommandIds.Add(commandId);
SettingsModel.SaveSettings(settings);
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
}
}
public CommandProviderContext GetProviderContext()
{
return new() { ProviderId = ProviderId };
}
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
public override int GetHashCode() => _commandProvider.GetHashCode();

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -31,7 +31,7 @@ public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings,
if (model.SettingsPage is not null)
{
SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost, provider.GetProviderContext());
SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost);
SettingsPage.InitializeProperties();
}
}

View File

@@ -1,9 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record PinCommandItemMessage(string ProviderId, string CommandId)
{
}

View File

@@ -18,8 +18,6 @@ public class ProviderSettings
public Dictionary<string, FallbackSettings> FallbackCommands { get; set; } = new();
public List<string> PinnedCommandIds { get; set; } = [];
[JsonIgnore]
public string ProviderDisplayName { get; set; } = string.Empty;

View File

@@ -22,7 +22,6 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class TopLevelCommandManager : ObservableObject,
IRecipient<ReloadCommandsMessage>,
IRecipient<PinCommandItemMessage>,
IPageContext,
IDisposable
{
@@ -43,7 +42,6 @@ public partial class TopLevelCommandManager : ObservableObject,
_commandProviderCache = commandProviderCache;
_taskScheduler = _serviceProvider.GetService<TaskScheduler>()!;
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
WeakReferenceMessenger.Default.Register<PinCommandItemMessage>(this);
_reloadCommandsGate = new(ReloadAllCommandsAsyncCore);
}
@@ -416,21 +414,6 @@ public partial class TopLevelCommandManager : ObservableObject,
public void Receive(ReloadCommandsMessage message) =>
ReloadAllCommandsAsync().ConfigureAwait(false);
public void Receive(PinCommandItemMessage message)
{
var wrapper = LookupProvider(message.ProviderId);
wrapper?.PinCommand(message.CommandId, _serviceProvider);
}
private CommandProviderWrapper? LookupProvider(string providerId)
{
lock (_commandProvidersLock)
{
return _builtInCommands.FirstOrDefault(w => w.ProviderId == providerId)
?? _extensionCommandProviders.FirstOrDefault(w => w.ProviderId == providerId);
}
}
void IPageContext.ShowException(Exception ex, string? extensionHint)
{
var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? "TopLevelCommandManager");

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -27,7 +27,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
private readonly IServiceProvider _serviceProvider;
private readonly CommandItemViewModel _commandItemViewModel;
public CommandProviderContext ProviderContext { get; private set; }
private readonly string _commandProviderId;
private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id;
@@ -57,7 +57,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
public CommandItemViewModel ItemViewModel => _commandItemViewModel;
public string CommandProviderId => ProviderContext.ProviderId;
public string CommandProviderId => _commandProviderId;
////// ICommandItem
public string Title => _commandItemViewModel.Title;
@@ -190,7 +190,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
CommandItemViewModel item,
bool isFallback,
CommandPaletteHost extensionHost,
CommandProviderContext commandProviderContext,
string commandProviderId,
SettingsModel settings,
ProviderSettings providerSettings,
IServiceProvider serviceProvider,
@@ -199,7 +199,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
_serviceProvider = serviceProvider;
_settings = settings;
_providerSettings = providerSettings;
ProviderContext = commandProviderContext;
_commandProviderId = commandProviderId;
_commandItemViewModel = item;
IsFallback = isFallback;
@@ -358,8 +358,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
{
// Use WyHash64 to generate stable ID hashes.
// manually seeding with 0, so that the hash is stable across launches
var result = WyHash64.ComputeHash64(CommandProviderId + DisplayTitle + Title + Subtitle, seed: 0);
_generatedId = $"{CommandProviderId}{result}";
var result = WyHash64.ComputeHash64(_commandProviderId + DisplayTitle + Title + Subtitle, seed: 0);
_generatedId = $"{_commandProviderId}{result}";
}
private void DoOnUiThread(Action action)

View File

@@ -11,42 +11,37 @@ public sealed class WindowPosition
/// <summary>
/// Gets or sets left position in device pixels.
/// </summary>
public int X { get; init; }
public int X { get; set; }
/// <summary>
/// Gets or sets top position in device pixels.
/// </summary>
public int Y { get; init; }
public int Y { get; set; }
/// <summary>
/// Gets or sets width in device pixels.
/// </summary>
public int Width { get; init; }
public int Width { get; set; }
/// <summary>
/// Gets or sets height in device pixels.
/// </summary>
public int Height { get; init; }
public int Height { get; set; }
/// <summary>
/// Gets or sets width of the screen in device pixels where the window is located.
/// </summary>
public int ScreenWidth { get; init; }
public int ScreenWidth { get; set; }
/// <summary>
/// Gets or sets height of the screen in device pixels where the window is located.
/// </summary>
public int ScreenHeight { get; init; }
public int ScreenHeight { get; set; }
/// <summary>
/// Gets or sets DPI (dots per inch) of the display where the window is located.
/// </summary>
public int Dpi { get; init; }
/// <summary>
/// Gets a value indicating whether the width and height of the window are valid (greater than 0).
/// </summary>
public bool IsSizeValid => Width > 0 && Height > 0;
public int Dpi { get; set; }
/// <summary>
/// Converts the window position properties to a <see cref="RectInt32"/> structure representing the physical window rectangle.

View File

@@ -18,7 +18,7 @@ internal static class WindowPositionHelper
private const int MinimumVisibleSize = 100;
private const int DefaultDpi = 96;
public static RectInt32? CenterOnDisplay(DisplayArea? displayArea, SizeInt32 windowSize, int windowDpi)
public static PointInt32? CalculateCenteredPosition(DisplayArea? displayArea, SizeInt32 windowSize, int windowDpi)
{
if (displayArea is null)
{
@@ -32,9 +32,15 @@ internal static class WindowPositionHelper
}
var targetDpi = GetDpiForDisplay(displayArea);
var scaledSize = ScaleSize(windowSize, windowDpi, targetDpi);
var clampedSize = ClampSize(scaledSize.Width, scaledSize.Height, workArea);
return CenterRectInWorkArea(clampedSize, workArea);
var predictedSize = ScaleSize(windowSize, windowDpi, targetDpi);
// Clamp to work area
var width = Math.Min(predictedSize.Width, workArea.Width);
var height = Math.Min(predictedSize.Height, workArea.Height);
return new PointInt32(
workArea.X + ((workArea.Width - width) / 2),
workArea.Y + ((workArea.Height - height) / 2));
}
/// <summary>
@@ -68,10 +74,6 @@ internal static class WindowPositionHelper
savedRect = savedRect with { Width = DefaultWidth, Height = DefaultHeight };
}
// Remember the original size before DPI scaling - needed to compute
// gaps relative to the old screen when repositioning across displays.
var originalSize = new SizeInt32(savedRect.Width, savedRect.Height);
if (targetDpi != savedDpi)
{
savedRect = ScaleRect(savedRect, savedDpi, targetDpi);
@@ -79,17 +81,12 @@ internal static class WindowPositionHelper
var clampedSize = ClampSize(savedRect.Width, savedRect.Height, workArea);
if (hasInvalidSize)
{
return CenterRectInWorkArea(clampedSize, workArea);
}
var shouldRecenter = hasInvalidSize ||
IsOffscreen(savedRect, workArea) ||
savedScreenSize.Width != workArea.Width ||
savedScreenSize.Height != workArea.Height;
if (savedScreenSize.Width != workArea.Width || savedScreenSize.Height != workArea.Height)
{
return RepositionRelativeToWorkArea(savedRect, savedScreenSize, originalSize, clampedSize, workArea);
}
if (IsOffscreen(savedRect, workArea))
if (shouldRecenter)
{
return CenterRectInWorkArea(clampedSize, workArea);
}
@@ -129,92 +126,27 @@ internal static class WindowPositionHelper
private static RectInt32 ScaleRect(RectInt32 rect, int fromDpi, int toDpi)
{
if (fromDpi <= 0 || toDpi <= 0 || fromDpi == toDpi)
{
return rect;
}
// Don't scale position, that's absolute coordinates in virtual screen space
var scale = (double)toDpi / fromDpi;
return new RectInt32(
rect.X,
rect.Y,
(int)Math.Round(rect.X * scale),
(int)Math.Round(rect.Y * scale),
(int)Math.Round(rect.Width * scale),
(int)Math.Round(rect.Height * scale));
}
private static SizeInt32 ClampSize(int width, int height, RectInt32 workArea)
{
return new SizeInt32(Math.Min(width, workArea.Width), Math.Min(height, workArea.Height));
}
private static SizeInt32 ClampSize(int width, int height, RectInt32 workArea) =>
new(Math.Min(width, workArea.Width), Math.Min(height, workArea.Height));
private static RectInt32 RepositionRelativeToWorkArea(RectInt32 savedRect, SizeInt32 savedScreenSize, SizeInt32 originalSize, SizeInt32 clampedSize, RectInt32 workArea)
{
// Treat each axis as a 3-zone grid (start / center / end) so that
// edge-snapped windows stay snapped and centered windows stay centered.
// We don't store the old work area origin, so we use the current one as a
// best estimate (correct when the same physical display changed resolution/DPI/taskbar).
var newX = ScaleAxisByZone(savedRect.X, originalSize.Width, clampedSize.Width, workArea.X, savedScreenSize.Width, workArea.Width);
var newY = ScaleAxisByZone(savedRect.Y, originalSize.Height, clampedSize.Height, workArea.Y, savedScreenSize.Height, workArea.Height);
newX = Math.Clamp(newX, workArea.X, Math.Max(workArea.X, workArea.X + workArea.Width - clampedSize.Width));
newY = Math.Clamp(newY, workArea.Y, Math.Max(workArea.Y, workArea.Y + workArea.Height - clampedSize.Height));
return new RectInt32(newX, newY, clampedSize.Width, clampedSize.Height);
}
/// <summary>
/// Repositions a window along one axis using a 3-zone model (start / center / end).
/// The zone is determined by which third of the old screen the window center falls in.
/// Uses <paramref name="oldWindowSize"/> (pre-DPI-scaling) for gap calculations against
/// the old screen, and <paramref name="newWindowSize"/> (post-scaling) for placement on the new screen.
/// </summary>
private static int ScaleAxisByZone(int savedPos, int oldWindowSize, int newWindowSize, int workAreaOrigin, int oldScreenSize, int newScreenSize)
{
if (oldScreenSize <= 0 || newScreenSize <= 0)
{
return savedPos;
}
var gapFromStart = savedPos - workAreaOrigin;
var windowCenter = gapFromStart + (oldWindowSize / 2);
if (windowCenter >= oldScreenSize / 3 && windowCenter <= oldScreenSize * 2 / 3)
{
// Center zone - keep centered
return workAreaOrigin + ((newScreenSize - newWindowSize) / 2);
}
var gapFromEnd = oldScreenSize - gapFromStart - oldWindowSize;
if (gapFromStart <= gapFromEnd)
{
// Start zone - preserve proportional distance from start edge
var rel = (double)gapFromStart / oldScreenSize;
return workAreaOrigin + (int)Math.Round(rel * newScreenSize);
}
else
{
// End zone - preserve proportional distance from end edge
var rel = (double)gapFromEnd / oldScreenSize;
return workAreaOrigin + newScreenSize - newWindowSize - (int)Math.Round(rel * newScreenSize);
}
}
private static RectInt32 CenterRectInWorkArea(SizeInt32 size, RectInt32 workArea)
{
return new RectInt32(
private static RectInt32 CenterRectInWorkArea(SizeInt32 size, RectInt32 workArea) =>
new(
workArea.X + ((workArea.Width - size.Width) / 2),
workArea.Y + ((workArea.Height - size.Height) / 2),
size.Width,
size.Height);
}
private static bool IsOffscreen(RectInt32 rect, RectInt32 workArea)
{
return rect.X + MinimumVisibleSize > workArea.X + workArea.Width ||
rect.X + rect.Width - MinimumVisibleSize < workArea.X ||
rect.Y + MinimumVisibleSize > workArea.Y + workArea.Height ||
rect.Y + rect.Height - MinimumVisibleSize < workArea.Y;
}
private static bool IsOffscreen(RectInt32 rect, RectInt32 workArea) =>
rect.X + MinimumVisibleSize > workArea.X + workArea.Width ||
rect.X + rect.Width - MinimumVisibleSize < workArea.X ||
rect.Y + MinimumVisibleSize > workArea.Y + workArea.Height ||
rect.Y + rect.Height - MinimumVisibleSize < workArea.Y;
}

View File

@@ -72,7 +72,6 @@ public sealed partial class MainWindow : WindowEx,
private readonly IThemeService _themeService;
private readonly WindowThemeSynchronizer _windowThemeSynchronizer;
private bool _ignoreHotKeyWhenFullScreen = true;
private bool _suppressDpiChange;
private bool _themeServiceInitialized;
// Session tracking for telemetry
@@ -128,16 +127,6 @@ public sealed partial class MainWindow : WindowEx,
_keyboardListener.SetProcessCommand(new CmdPalKeyboardService.ProcessCommand(HandleSummon));
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
// member (and instead like, use a local), then the pointer we marshal
// into the WindowLongPtr will be useless after we leave this function,
// and our **WindProc will explode**.
_hotkeyWndProc = HotKeyPrc;
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
this.SetIcon();
AppWindow.Title = RS_.GetString("AppName");
RestoreWindowPosition();
@@ -164,6 +153,16 @@ public sealed partial class MainWindow : WindowEx,
SizeChanged += WindowSizeChanged;
RootElement.Loaded += RootElementLoaded;
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
// member (and instead like, use a local), then the pointer we marshal
// into the WindowLongPtr will be useless after we leave this function,
// and our **WindProc will explode**.
_hotkeyWndProc = HotKeyPrc;
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
// Load our settings, and then also wire up a settings changed handler
HotReloadSettings();
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
@@ -214,11 +213,6 @@ public sealed partial class MainWindow : WindowEx,
// Now that our content has loaded, we can update our draggable regions
UpdateRegionsForCustomTitleBar();
// Also update regions when DPI changes. SizeChanged only fires when the logical
// (DIP) size changes — a DPI change that scales the physical size while preserving
// the DIP size won't trigger it, leaving drag regions at the old physical coordinates.
RootElement.XamlRoot.Changed += XamlRoot_Changed;
// Add dev ribbon if enabled
if (!BuildInfo.IsCiBuild)
{
@@ -227,8 +221,6 @@ public sealed partial class MainWindow : WindowEx,
}
}
private void XamlRoot_Changed(XamlRoot sender, XamlRootChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
private void PositionCentered()
@@ -239,14 +231,16 @@ public sealed partial class MainWindow : WindowEx,
private void PositionCentered(DisplayArea displayArea)
{
var rect = WindowPositionHelper.CenterOnDisplay(
var position = WindowPositionHelper.CalculateCenteredPosition(
displayArea,
AppWindow.Size,
(int)this.GetDpiForWindow());
if (rect is not null)
if (position is not null)
{
MoveAndResizeDpiAware(rect.Value);
// Use Move(), not MoveAndResize(). Windows auto-resizes on DPI change via WM_DPICHANGED;
// the helper already accounts for this when calculating the centered position.
AppWindow.Move((PointInt32)position);
}
}
@@ -255,62 +249,29 @@ public sealed partial class MainWindow : WindowEx,
var settings = App.Current.Services.GetService<SettingsModel>();
if (settings?.LastWindowPosition is not { Width: > 0, Height: > 0 } savedPosition)
{
// don't try to restore if the saved position is invalid, just recenter
PositionCentered();
return;
}
// MoveAndResize is safe here—we're restoring a saved state at startup,
// not moving a live window between displays.
var newRect = WindowPositionHelper.AdjustRectForVisibility(
savedPosition.ToPhysicalWindowRectangle(),
new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight),
savedPosition.Dpi);
MoveAndResizeDpiAware(newRect);
}
/// <summary>
/// Moves and resizes the window while suppressing WM_DPICHANGED.
/// The caller is expected to provide a rect already scaled for the target display's DPI.
/// Without suppression, the framework would apply its own DPI scaling on top, double-scaling the window.
/// </summary>
private void MoveAndResizeDpiAware(RectInt32 rect)
{
var originalMinHeight = MinHeight;
var originalMinWidth = MinWidth;
_suppressDpiChange = true;
try
{
// WindowEx is uses current DPI to calculate the minimum window size
MinHeight = 0;
MinWidth = 0;
AppWindow.MoveAndResize(rect);
}
finally
{
MinHeight = originalMinHeight;
MinWidth = originalMinWidth;
_suppressDpiChange = false;
}
AppWindow.MoveAndResize(newRect);
}
private void UpdateWindowPositionInMemory()
{
var placement = new WINDOWPLACEMENT { length = (uint)Marshal.SizeOf<WINDOWPLACEMENT>() };
if (!PInvoke.GetWindowPlacement(_hwnd, ref placement))
{
return;
}
var rect = placement.rcNormalPosition;
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
_currentWindowPosition = new WindowPosition
{
X = rect.X,
Y = rect.Y,
Width = rect.Width,
Height = rect.Height,
X = AppWindow.Position.X,
Y = AppWindow.Position.Y,
Width = AppWindow.Size.Width,
Height = AppWindow.Size.Height,
Dpi = (int)this.GetDpiForWindow(),
ScreenWidth = displayArea.WorkArea.Width,
ScreenHeight = displayArea.WorkArea.Height,
@@ -519,7 +480,7 @@ public sealed partial class MainWindow : WindowEx,
{
var originalScreen = new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight);
var newRect = WindowPositionHelper.AdjustRectForVisibility(_currentWindowPosition.ToPhysicalWindowRectangle(), originalScreen, _currentWindowPosition.Dpi);
MoveAndResizeDpiAware(newRect);
AppWindow.MoveAndResize(newRect);
}
else
{
@@ -776,12 +737,18 @@ public sealed partial class MainWindow : WindowEx,
var settings = serviceProvider.GetService<SettingsModel>();
if (settings is not null)
{
// a quick sanity check, so we don't overwrite correct values
if (_currentWindowPosition.IsSizeValid)
settings.LastWindowPosition = new WindowPosition
{
settings.LastWindowPosition = _currentWindowPosition;
SettingsModel.SaveSettings(settings);
}
X = _currentWindowPosition.X,
Y = _currentWindowPosition.Y,
Width = _currentWindowPosition.Width,
Height = _currentWindowPosition.Height,
Dpi = _currentWindowPosition.Dpi,
ScreenWidth = _currentWindowPosition.ScreenWidth,
ScreenHeight = _currentWindowPosition.ScreenHeight,
};
SettingsModel.SaveSettings(settings);
}
var extensionService = serviceProvider.GetService<IExtensionService>()!;
@@ -1141,13 +1108,6 @@ public sealed partial class MainWindow : WindowEx,
// Prevent the window from maximizing when double-clicking the title bar area
case PInvoke.WM_NCLBUTTONDBLCLK:
return (LRESULT)IntPtr.Zero;
// When restoring a saved position across monitors with different DPIs,
// MoveAndResize already sets the correctly-scaled size. Suppress the
// framework's automatic DPI resize to avoid double-scaling.
case PInvoke.WM_DPICHANGED when _suppressDpiChange:
return (LRESULT)IntPtr.Zero;
case PInvoke.WM_HOTKEY:
{
var hotkeyIndex = (int)wParam.Value;

View File

@@ -66,8 +66,4 @@ GetStockObject
GetModuleHandle
GetWindowThreadProcessId
AttachThreadInput
GetWindowPlacement
WINDOWPLACEMENT
WM_DPICHANGED
AttachThreadInput

View File

@@ -26,15 +26,4 @@ internal sealed class PowerToysAppHostService : IAppHostService
return topLevelHost ?? currentHost ?? CommandPaletteHost.Instance;
}
public CommandProviderContext GetProviderContextForCommand(object? command, CommandProviderContext? currentContext)
{
CommandProviderContext? topLevelId = null;
if (command is TopLevelViewModel topLevelViewModel)
{
topLevelId = topLevelViewModel.ProviderContext;
}
return topLevelId ?? currentContext ?? throw new InvalidOperationException("No command provider context could be found for the given command, and no current context was provided.");
}
}

View File

@@ -795,10 +795,4 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>K</value>
<comment>Keyboard key</comment>
</data>
<data name="ConfigureShortcut" xml:space="preserve">
<value>Configure shortcut</value>
</data>
<data name="ConfigureShortcutText.Text" xml:space="preserve">
<value>Assign shortcut</value>
</data>
</root>

View File

@@ -6,42 +6,42 @@ namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer;
public partial class ErrorReportSanitizerTests
{
internal static class TestData
private static class TestData
{
internal static string Input =>
$"""
HRESULT: 0x80004005
HRESULT: -2147467259
$"""
HRESULT: 0x80004005
HRESULT: -2147467259
Here is e-mail address <jane.doe@contoso.com>
IPv4 address: 192.168.100.1
IPv4 loopback address: 127.0.0.1
MAC address: 00-14-22-01-23-45
IPv6 address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
IPv6 loopback address: ::1
Password: P@ssw0rd123!
Password=secret
Api key: 1234567890abcdef
PostgreSQL connection string: Host=localhost;Username=postgres;Password=secret;Database=mydb
InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;
X-API-key: 1234567890abcdef
Pet-Shop-Subscription-Key: 1234567890abcdef
Here is a user name {Environment.UserName}
And here is a profile path {Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\RandomFolder
Here is a local app data path {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal
Here is machine name {Environment.MachineName}
JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
User email john.doe@company.com failed validation
File not found: {Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\\secret.txt
Connection string: Server=localhost;User ID=admin;Password=secret123;Database=test
Phone number 555-123-4567 is invalid
API key abc123def456ghi789jkl012mno345pqr678 expired
Failed to connect to https://api.internal-company.com/users/12345?token=secret_abc123
Error accessing file://C:/Users/john.doe/Documents/confidential.pdf
JDBC connection failed: jdbc://database-server:5432/userdb?user=admin&password=secret
FTP upload error: ftp://internal-server.company.com/uploads/user_data.csv
Email service error: mailto:admin@internal-company.com?subject=Alert
""";
Here is e-mail address <jane.doe@contoso.com>
IPv4 address: 192.168.100.1
IPv4 loopback address: 127.0.0.1
MAC address: 00-14-22-01-23-45
IPv6 address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
IPv6 loopback address: ::1
Password: P@ssw0rd123!
Password=secret
Api key: 1234567890abcdef
PostgreSQL connection string: Host=localhost;Username=postgres;Password=secret;Database=mydb
InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;
X-API-key: 1234567890abcdef
Pet-Shop-Subscription-Key: 1234567890abcdef
Here is a user name {Environment.UserName}
And here is a profile path {Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\RandomFolder
Here is a local app data path {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal
Here is machine name {Environment.MachineName}
JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
User email john.doe@company.com failed validation
File not found: {Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\\secret.txt
Connection string: Server=localhost;User ID=admin;Password=secret123;Database=test
Phone number 555-123-4567 is invalid
API key abc123def456ghi789jkl012mno345pqr678 expired
Failed to connect to https://api.internal-company.com/users/12345?token=secret_abc123
Error accessing file://C:/Users/john.doe/Documents/confidential.pdf
JDBC connection failed: jdbc://database-server:5432/userdb?user=admin&password=secret
FTP upload error: ftp://internal-server.company.com/uploads/user_data.csv
Email service error: mailto:admin@internal-company.com?subject=Alert
""";
public const string Expected =
$"""
@@ -49,8 +49,8 @@ public partial class ErrorReportSanitizerTests
HRESULT: -2147467259
Here is e-mail address <[EMAIL_REDACTED]>
IPv4 address: 192.168.100.1
IPv4 loopback address: 127.0.0.1
IPv4 address: [IP4_REDACTED]
IPv4 loopback address: [IP4_REDACTED]
MAC address: [MAC_ADDRESS_REDACTED]
IPv6 address: [IP6_REDACTED]
IPv6 loopback address: [IP6_REDACTED]
@@ -77,55 +77,5 @@ public partial class ErrorReportSanitizerTests
FTP upload error: [URL_REDACTED]
Email service error: mailto:[EMAIL_REDACTED]?subject=Alert
""";
internal static string Input2 =>
$"""
============================================================
Hello World! Command Palette is starting.
Application:
App version: 0.0.1.0
Packaging flavor: Packaged
Is elevated: no
Environment:
OS version: Microsoft Windows 10.0.26220
OS architecture: X64
Runtime identifier: win-x64
Framework: .NET 9.0.13
Process architecture: X64
Culture: cs-CZ
UI culture: en-US
Paths:
Log directory: {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal\Logs\0.0.1.0
Config directory: {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Packages\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe\LocalState
============================================================
""";
public const string Expected2 =
"""
============================================================
Hello World! Command Palette is starting.
Application:
App version: 0.0.1.0
Packaging flavor: Packaged
Is elevated: no
Environment:
OS version: Microsoft Windows 10.0.26220
OS architecture: X64
Runtime identifier: win-x64
Framework: .NET 9.0.13
Process architecture: X64
Culture: cs-CZ
UI culture: en-US
Paths:
Log directory: [LOCALAPPLICATIONDATA_DIR]Microsoft\PowerToys\CmdPal\Logs\0.0.1.0
Config directory: [LOCALAPPLICATIONDATA_DIR]Packages\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe\LocalState
============================================================
""";
}
}

View File

@@ -22,18 +22,4 @@ public partial class ErrorReportSanitizerTests
// Assert
Assert.AreEqual(TestData.Expected, result);
}
[TestMethod]
public void Sanitize_ShouldNotMaskTooMuchPiiInErrorReport()
{
// Arrange
var reportSanitizer = new ErrorReportSanitizer();
var input = TestData.Input2;
// Act
var result = reportSanitizer.Sanitize(input);
// Assert
Assert.AreEqual(TestData.Expected2, result);
}
}

View File

@@ -1,62 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.UnitTests.TestUtils;
using Microsoft.CmdPal.Core.Common.Services.Sanitizer;
using Microsoft.CmdPal.Core.Common.Services.Sanitizer.Abstraction;
namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer;
[TestClass]
public class FilenameMaskRuleProviderTests
{
[TestMethod]
public void GetRules_ShouldReturnExpectedRules()
{
// Arrange
var provider = new FilenameMaskRuleProvider();
// Act
var rules = provider.GetRules();
// Assert
var ruleList = new List<SanitizationRule>(rules);
Assert.AreEqual(1, ruleList.Count);
Assert.AreEqual("Mask filename in any path", ruleList[0].Description);
}
[DataTestMethod]
[DataRow(@"C:\Users\Alice\Documents\secret.txt", @"C:\Users\Alice\Documents\se****.txt")]
[DataRow(@"logs\error-report.log", @"logs\er**********.log")]
[DataRow(@"/var/logs/trace.json", @"/var/logs/tr***.json")]
public void FilenameRules_ShouldMaskFileNamesInPaths(string input, string expected)
{
// Arrange
var provider = new FilenameMaskRuleProvider();
// Act
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
// Assert
Assert.AreEqual(expected, result);
}
[DataTestMethod]
[DataRow("C:\\Users\\Alice\\Documents\\", "C:\\Users\\Alice\\Documents\\")]
[DataRow(@"C:\Users\Alice\PowerToys\CmdPal\Logs\1.2.3.4", @"C:\Users\Alice\PowerToys\CmdPal\Logs\1.2.3.4")]
[DataRow(@"C:\Users\Alice\appsettings.json", @"C:\Users\Alice\appsettings.json")]
[DataRow(@"C:\Users\Alice\.env", @"C:\Users\Alice\.env")]
[DataRow(@"logs\readme", @"logs\readme")]
public void FilenameRules_ShouldNotMaskNonSensitivePatterns(string input, string expected)
{
// Arrange
var provider = new FilenameMaskRuleProvider();
// Act
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
// Assert
Assert.AreEqual(expected, result);
}
}

View File

@@ -54,8 +54,6 @@ public class PiiRuleProviderTests
[DataRow("Two numbers: 123-456-7890 and +420 777123456", "Two numbers: [PHONE_REDACTED] and [PHONE_REDACTED]")]
[DataRow("Czech phone +420 777 123 456", "Czech phone [PHONE_REDACTED]")]
[DataRow("Slovak phone +421 777 12 34 56", "Slovak phone [PHONE_REDACTED]")]
[DataRow("Version 1.2.3.4", "Version 1.2.3.4")]
[DataRow("OS version: Microsoft Windows 10.0.26220", "OS version: Microsoft Windows 10.0.26220")]
[DataRow("No phone number here", "No phone number here")]
public void PhoneRules_ShouldMaskPhoneNumbers(string input, string expected)
{
@@ -106,8 +104,6 @@ public class PiiRuleProviderTests
[DataRow("GUID: 123e4567-e89b-12d3-a456-426614174000", "GUID: 123e4567-e89b-12d3-a456-426614174000")]
[DataRow("Timestamp: 2023-10-05T14:32:10Z", "Timestamp: 2023-10-05T14:32:10Z")]
[DataRow("Version: 1.2.3", "Version: 1.2.3")]
[DataRow("Version: 1.2.3.4", "Version: 1.2.3.4")]
[DataRow("Version: 0.2.3.4", "Version: 0.2.3.4")]
[DataRow("Version: 10.0.22631.3448", "Version: 10.0.22631.3448")]
[DataRow("MAC: 00:1A:2B:3C:4D:5E", "MAC: 00:1A:2B:3C:4D:5E")]
[DataRow("Date: 2023-10-05", "Date: 2023-10-05")]

View File

@@ -77,7 +77,6 @@ functionality.
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
- [Addenda I: API additions (ICommandProvider2)](#addenda-i-api-additions-icommandprovider2)
- [Addenda IV: Dock bands](#addenda-iv-dock-bands)
- [Pinning nested commands to the dock (and top level)](#pinning-nested-commands-to-the-dock-and-top-level)
- [Class diagram](#class-diagram)
- [Future considerations](#future-considerations)
- [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments)
@@ -2129,36 +2128,6 @@ Users may choose to have:
- Dock bands will still display the `Title` & `Subtitle` of each item in the
band as the tooltip on those items, even when the "labels" are hidden.
### Pinning nested commands to the dock (and top level)
We'll use another command provider method to allow the host to ask extensions
for items based on their ID.
```csharp
interface ICommandProvider4 requires ICommandProvider3
{
ICommandItem GetCommandItem(String id);
};
```
This will allow users to pin not just top-level commands, but also nested
commands which have an ID. The host can store that ID away, and then later ask
the extension for the `ICommandItem` with that ID, to get the full details of
the command to pin.
This is needed separate from the `GetCommand` method on `ICommandProvider`,
because that method is was designed for two main purposes:
* Short-circuiting the loading of top-level commands for frozen extensions. In
that case, DevPal would only need to look up the actual `ICommand` to perform
it. It wouldn't need the full `ICommandItem` with all the details.
* Allowing invokable commands to navigate using the GoToPageArgs. In that case,
DevPal would only need the `ICommand` to perform the navigation.
In neither of those scenarios was the full "display" of the item needed. In
pinning scenarios, however, we need everything that the user would see in the UI
for that item, which is all in the `ICommandItem`.
## Class diagram
This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.)

View File

@@ -184,20 +184,6 @@ public partial class AllAppsCommandProvider : CommandProvider
return null;
}
public override ICommandItem? GetCommandItem(string id)
{
var items = _page.GetItems();
foreach (var item in items)
{
if (item.Command.Id == id)
{
return item;
}
}
return null;
}
private void OnPinStateChanged(object? sender, System.EventArgs e)
{
RaiseItemsChanged(0);

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -42,61 +42,37 @@ internal static class Commands
var results = new List<IListItem>();
results.AddRange(new[]
{
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_shutdown, confirmCommands, Resources.Microsoft_plugin_sys_shutdown_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/s /hybrid /t 0", runWithHiddenWindow: true))
{
Id = "com.microsoft.cmdpal.builtin.system.shutdown",
})
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_shutdown, confirmCommands, Resources.Microsoft_plugin_sys_shutdown_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/s /hybrid /t 0", runWithHiddenWindow: true)))
{
Title = Resources.Microsoft_plugin_sys_shutdown_computer,
Subtitle = Resources.Microsoft_plugin_sys_shutdown_computer_description,
Icon = Icons.ShutdownIcon,
},
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_restart, confirmCommands, Resources.Microsoft_plugin_sys_restart_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/g /t 0", runWithHiddenWindow: true))
{
Id = "com.microsoft.cmdpal.builtin.system.restart",
})
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_restart, confirmCommands, Resources.Microsoft_plugin_sys_restart_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/g /t 0", runWithHiddenWindow: true)))
{
Title = Resources.Microsoft_plugin_sys_restart_computer,
Subtitle = Resources.Microsoft_plugin_sys_restart_computer_description,
Icon = Icons.RestartIcon,
},
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_signout, confirmCommands, Resources.Microsoft_plugin_sys_sign_out_confirmation, () => NativeMethods.ExitWindowsEx(EWXLOGOFF, 0))
{
Id = "com.microsoft.cmdpal.builtin.system.signout",
})
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_signout, confirmCommands, Resources.Microsoft_plugin_sys_sign_out_confirmation, () => NativeMethods.ExitWindowsEx(EWXLOGOFF, 0)))
{
Title = Resources.Microsoft_plugin_sys_sign_out,
Subtitle = Resources.Microsoft_plugin_sys_sign_out_description,
Icon = Icons.LogoffIcon,
},
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_lock, confirmCommands, Resources.Microsoft_plugin_sys_lock_confirmation, () => NativeMethods.LockWorkStation())
{
Id = "com.microsoft.cmdpal.builtin.system.lock",
})
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_lock, confirmCommands, Resources.Microsoft_plugin_sys_lock_confirmation, () => NativeMethods.LockWorkStation()))
{
Title = Resources.Microsoft_plugin_sys_lock,
Subtitle = Resources.Microsoft_plugin_sys_lock_description,
Icon = Icons.LockIcon,
},
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_sleep, confirmCommands, Resources.Microsoft_plugin_sys_sleep_confirmation, () => NativeMethods.SetSuspendState(false, true, true))
{
Id = "com.microsoft.cmdpal.builtin.system.sleep",
})
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_sleep, confirmCommands, Resources.Microsoft_plugin_sys_sleep_confirmation, () => NativeMethods.SetSuspendState(false, true, true)))
{
Title = Resources.Microsoft_plugin_sys_sleep,
Subtitle = Resources.Microsoft_plugin_sys_sleep_description,
Icon = Icons.SleepIcon,
},
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_hibernate, confirmCommands, Resources.Microsoft_plugin_sys_hibernate_confirmation, () => NativeMethods.SetSuspendState(true, true, true))
{
Id = "com.microsoft.cmdpal.builtin.system.hibernate",
})
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_hibernate, confirmCommands, Resources.Microsoft_plugin_sys_hibernate_confirmation, () => NativeMethods.SetSuspendState(true, true, true)))
{
Title = Resources.Microsoft_plugin_sys_hibernate,
Subtitle = Resources.Microsoft_plugin_sys_hibernate_description,
@@ -109,19 +85,13 @@ internal static class Commands
{
results.AddRange(new[]
{
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder")
{
Id = "com.microsoft.cmdpal.builtin.system.recycle_bin",
})
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder"))
{
Title = Resources.Microsoft_plugin_sys_RecycleBinOpen,
Subtitle = Resources.Microsoft_plugin_sys_RecycleBin_description,
Icon = Icons.RecycleBinIcon,
},
new ListItem(new EmptyRecycleBinConfirmation(emptyRBSuccessMessage)
{
Id = "com.microsoft.cmdpal.builtin.system.empty_recycle_bin",
})
new ListItem(new EmptyRecycleBinConfirmation(emptyRBSuccessMessage))
{
Title = Resources.Microsoft_plugin_sys_RecycleBinEmptyResult,
Subtitle = Resources.Microsoft_plugin_sys_RecycleBinEmpty_description,
@@ -132,10 +102,7 @@ internal static class Commands
else
{
results.Add(
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder")
{
Id = "com.microsoft.cmdpal.builtin.system.recycle_bin",
})
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder"))
{
Title = Resources.Microsoft_plugin_sys_RecycleBin,
Subtitle = Resources.Microsoft_plugin_sys_RecycleBin_description,
@@ -143,15 +110,7 @@ internal static class Commands
});
}
results.Add(new ListItem(
new ExecuteCommandConfirmation(
Resources.Microsoft_plugin_sys_RestartShell_name!,
confirmCommands,
Resources.Microsoft_plugin_sys_RestartShell_confirmation!,
static () => OpenInShellHelper.OpenInShell("cmd", "/C tskill explorer && start explorer", runWithHiddenWindow: true))
{
Id = "com.microsoft.cmdpal.builtin.system.restart_shell",
})
results.Add(new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_sys_RestartShell_name!, confirmCommands, Resources.Microsoft_plugin_sys_RestartShell_confirmation!, static () => OpenInShellHelper.OpenInShell("cmd", "/C tskill explorer && start explorer", runWithHiddenWindow: true)))
{
Title = Resources.Microsoft_plugin_sys_RestartShell!,
Subtitle = Resources.Microsoft_plugin_sys_RestartShell_description!,
@@ -182,19 +141,19 @@ internal static class Commands
var results = new List<IListItem>();
// We update the cache only if the last query is older than 'updateCacheIntervalSeconds' seconds
var timeOfLastNetworkQueryBefore = timeOfLastNetworkQuery;
DateTime timeOfLastNetworkQueryBefore = timeOfLastNetworkQuery;
timeOfLastNetworkQuery = DateTime.Now; // Set time of last query to this query
if ((timeOfLastNetworkQuery - timeOfLastNetworkQueryBefore).TotalSeconds >= UpdateCacheIntervalSeconds)
{
networkPropertiesCache = NetworkConnectionProperties.GetList();
}
var sysIpv4DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip4_description);
var sysIpv6DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip6_description);
var sysMacDescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_mac_description);
CompositeFormat sysIpv4DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip4_description);
CompositeFormat sysIpv6DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip6_description);
CompositeFormat sysMacDescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_mac_description);
var hideDisconnectedNetworkInfo = manager.HideDisconnectedNetworkInfo();
foreach (var intInfo in networkPropertiesCache)
foreach (NetworkConnectionProperties intInfo in networkPropertiesCache)
{
if (hideDisconnectedNetworkInfo)
{

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -39,18 +39,4 @@ public sealed partial class SystemCommandExtensionProvider : CommandProvider
}
public override IFallbackCommandItem[] FallbackCommands() => [_fallbackSystemItem];
public override ICommandItem? GetCommandItem(string id)
{
var everything = Page.GetItems();
foreach (var item in everything)
{
if (item.Command.Id == id)
{
return item;
}
}
return null;
}
}

View File

@@ -2,8 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
@@ -28,22 +26,6 @@ internal static class BrowserInfoServiceExtensions
/// </remarks>
public static bool Open(this IBrowserInfoService browserInfoService, string url)
{
// If the URL is a valid URI, attempt to open it with the default browser by invoking it through the shell.
if (Uri.TryCreate(url, UriKind.Absolute, out _))
{
try
{
ShellHelpers.OpenInShell(url);
return true;
}
catch (Exception ex)
{
Logger.LogDebug($"Failed to launch the URI {url}: {ex}");
}
}
// Use legacy method to open the URL if it's not a well-formed URI or if the shell launch fails.
// This may handle cases where the URL is a search query or a custom URI scheme.
var defaultBrowser = browserInfoService.GetDefaultBrowser();
return defaultBrowser != null && ShellHelpers.OpenCommandInShell(defaultBrowser.Path, defaultBrowser.ArgumentsPattern, url);
}

View File

@@ -395,11 +395,7 @@ internal sealed class Window
var childProcessName = WindowProcess.GetProcessNameFromProcessID(childProcessId);
// Update process info in cache
lock (_handlesToProcessCache)
{
_handlesToProcessCache[hWindow].UpdateProcessInfo(childProcessId, childThreadId, childProcessName);
}
_handlesToProcessCache[hWindow].UpdateProcessInfo(childProcessId, childThreadId, childProcessName);
return false;
}
else

View File

@@ -9,8 +9,7 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
public abstract partial class CommandProvider :
ICommandProvider,
ICommandProvider2,
ICommandProvider3,
ICommandProvider4
ICommandProvider3
{
public virtual string Id { get; protected set; } = string.Empty;
@@ -26,8 +25,6 @@ public abstract partial class CommandProvider :
public virtual ICommand? GetCommand(string id) => null;
public virtual ICommandItem? GetCommandItem(string id) => null;
public virtual ICommandSettings? Settings { get; protected set; }
public virtual bool Frozen { get; protected set; } = true;

View File

@@ -411,11 +411,5 @@ namespace Microsoft.CommandPalette.Extensions
{
ICommandItem[] GetDockBands();
};
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface ICommandProvider4 requires ICommandProvider3
{
ICommandItem GetCommandItem(String id);
};
}

View File

@@ -404,11 +404,7 @@ namespace Microsoft.Plugin.WindowWalker.Components
var childProcessName = WindowProcess.GetProcessNameFromProcessID(childProcessId);
// Update process info in cache
lock (_handlesToProcessCache)
{
_handlesToProcessCache[hWindow].UpdateProcessInfo(childProcessId, childThreadId, childProcessName);
}
_handlesToProcessCache[hWindow].UpdateProcessInfo(childProcessId, childThreadId, childProcessName);
return false;
}
else

View File

@@ -152,18 +152,6 @@
x:Uid="AdvancedPasteUI_Shortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"
IsExpanded="True">
<tkcontrols:SettingsExpander.ItemsHeader>
<InfoBar
x:Uid="GPO_SettingIsManaged"
IsClosable="False"
IsOpen="{x:Bind ViewModel.ShowClipboardHistoryIsGpoConfiguredInfoBar, Mode=OneWay}"
IsTabStop="{x:Bind ViewModel.ShowClipboardHistoryIsGpoConfiguredInfoBar, Mode=OneWay}"
Severity="Informational">
<InfoBar.IconSource>
<FontIconSource FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="&#xE72E;" />
</InfoBar.IconSource>
</InfoBar>
</tkcontrols:SettingsExpander.ItemsHeader>
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.AdvancedPasteUIShortcut, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard Name="AdvancedPasteEnableClipboardPreview" ContentAlignment="Left">
@@ -178,7 +166,16 @@
IsEnabled="{x:Bind ViewModel.ClipboardHistoryEnabled, Mode=OneWay}">
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="AdvancedPaste_Clipboard_History_Enabled_SettingsCard" IsChecked="{x:Bind ViewModel.ClipboardHistoryEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<InfoBar
x:Uid="GPO_SettingIsManaged"
IsClosable="False"
IsOpen="{x:Bind ViewModel.ShowClipboardHistoryIsGpoConfiguredInfoBar, Mode=OneWay}"
IsTabStop="{x:Bind ViewModel.ShowClipboardHistoryIsGpoConfiguredInfoBar, Mode=OneWay}"
Severity="Informational">
<InfoBar.IconSource>
<FontIconSource FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="&#xE72E;" />
</InfoBar.IconSource>
</InfoBar>
<tkcontrols:SettingsCard Name="AdvancedPasteCloseAfterLosingFocus" ContentAlignment="Left">
<CheckBox x:Uid="AdvancedPaste_CloseAfterLosingFocus" IsChecked="{x:Bind ViewModel.CloseAfterLosingFocus, Mode=TwoWay}" />
</tkcontrols:SettingsCard>