diff --git a/.pipelines/ESRPSigning_advancedpaste_msix_content.json b/.pipelines/ESRPSigning_advancedpaste_msix_content.json
new file mode 100644
index 000000000000..6522fc56b367
--- /dev/null
+++ b/.pipelines/ESRPSigning_advancedpaste_msix_content.json
@@ -0,0 +1,51 @@
+{
+ "Version": "1.0.0",
+ "UseMinimatch": false,
+ "SignBatches": [
+ {
+ "MatchedPath": [
+ "*.dll",
+ "*.exe"
+ ],
+ "SigningInfo": {
+ "Operations": [
+ {
+ "KeyCode": "CP-230012",
+ "OperationSetCode": "SigntoolSign",
+ "Parameters": [
+ {
+ "parameterName": "OpusName",
+ "parameterValue": "Microsoft"
+ },
+ {
+ "parameterName": "OpusInfo",
+ "parameterValue": "http://www.microsoft.com"
+ },
+ {
+ "parameterName": "FileDigest",
+ "parameterValue": "/fd \"SHA256\""
+ },
+ {
+ "parameterName": "PageHash",
+ "parameterValue": "/NPH"
+ },
+ {
+ "parameterName": "TimeStamp",
+ "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
+ }
+ ],
+ "ToolName": "sign",
+ "ToolVersion": "1.0"
+ },
+ {
+ "KeyCode": "CP-230012",
+ "OperationSetCode": "SigntoolVerify",
+ "Parameters": [],
+ "ToolName": "sign",
+ "ToolVersion": "1.0"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index 09d65893d717..f264f5a1b270 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -49,8 +49,7 @@
"PowerToys.PowerOCR.exe",
"PowerToys.AdvancedPasteModuleInterface.dll",
- "WinUI3Apps\\PowerToys.AdvancedPaste.exe",
- "WinUI3Apps\\PowerToys.AdvancedPaste.dll",
+ "WinUI3Apps\\AdvancedPaste\\*PowerToys.AdvancedPaste*.msix",
"PowerToys.AwakeModuleInterface.dll",
"PowerToys.Awake.exe",
diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml
index d3adc45f047f..c6016c136433 100644
--- a/.pipelines/v2/release.yml
+++ b/.pipelines/v2/release.yml
@@ -102,7 +102,7 @@ extends:
useManagedIdentity: $(SigningUseManagedIdentity)
clientId: $(SigningOriginalClientId)
# Have msbuild use the release nuget config profile
- additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:EnableCmdPalAOT=true
+ additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:EnableCmdPalAOT=true /p:PhiSilicaLafToken=$(PhiSilicaLafToken) /p:PhiSilicaLafAttestation="$(PhiSilicaLafAttestation)"
beforeBuildSteps:
# Sets versions for all PowerToy created DLLs
- pwsh: |-
diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml
index 1aeb799a65f2..45a95897734b 100644
--- a/.pipelines/v2/templates/job-build-project.yml
+++ b/.pipelines/v2/templates/job-build-project.yml
@@ -269,6 +269,9 @@ jobs:
- template: .\steps-setup-versioning.yml
parameters:
directory: $(build.sourcesdirectory)\src\modules\cmdpal
+ - template: .\steps-setup-versioning.yml
+ parameters:
+ directory: $(build.sourcesdirectory)\src\modules\AdvancedPaste
@@ -484,6 +487,46 @@ jobs:
**\*UnitTest*.dll
!**\obj\**
+ - pwsh: |-
+ $SearchRoot = "$(Build.SourcesDirectory)/$(BuildPlatform)/$(BuildConfiguration)"
+ Write-Host "Searching for AdvancedPaste MSIX in: $SearchRoot"
+ if (-not (Test-Path $SearchRoot)) {
+ Write-Warning "Search root does not exist: $SearchRoot"
+ $SearchRoot = "$(Build.SourcesDirectory)"
+ Write-Host "Falling back to: $SearchRoot"
+ }
+ # Log the AdvancedPaste output folder structure for diagnostics
+ $apDir = Join-Path $SearchRoot "WinUI3Apps/AdvancedPaste"
+ if (Test-Path $apDir) {
+ Write-Host "AdvancedPaste output folder contents:"
+ Get-ChildItem $apDir -Recurse -Include "*.msix","*.appx" | ForEach-Object { Write-Host " - $($_.FullName)" }
+ } else {
+ Write-Host "AdvancedPaste output folder not found at: $apDir"
+ }
+ $Packages = Get-ChildItem -Path $SearchRoot -Recurse -Filter "PowerToys.AdvancedPaste*.msix" -ErrorAction SilentlyContinue
+ Write-Host "Found $($Packages.Count) AdvancedPaste MSIX package(s):"
+ foreach ($pkg in $Packages) {
+ Write-Host " - $($pkg.FullName)"
+ }
+
+ if ($Packages.Count -gt 0) {
+ $PlatformPackage = $Packages | Where-Object { $_.Name -match "PowerToys\.AdvancedPaste.*_(x64|arm64)\.msix$" } | Select-Object -First 1
+ if ($PlatformPackage) {
+ $Package = $PlatformPackage
+ Write-Host "Using platform-specific package: $($Package.FullName)"
+ } else {
+ $Package = $Packages | Select-Object -First 1
+ Write-Host "Using first available package: $($Package.FullName)"
+ }
+
+ $PackageFilename = $Package.FullName
+ Write-Host "##vso[task.setvariable variable=AdvancedPastePackagePath]${PackageFilename}"
+ } else {
+ Write-Error "No AdvancedPaste MSIX packages found! The build may have failed to generate the MSIX."
+ exit 1
+ }
+ displayName: Locate the AdvancedPaste MSIX
+
- pwsh: |-
$Packages = Get-ChildItem -Recurse -Filter "Microsoft.CmdPal.UI_*.msix"
Write-Host "Found $($Packages.Count) CmdPal MSIX package(s):"
@@ -533,6 +576,29 @@ jobs:
Remove-Item -Force -Recurse "$(JobOutputDirectory)/_appx" -ErrorAction:Ignore
displayName: Re-pack the new CmdPal package after signing
+ - pwsh: |-
+ & "$(MakeAppxPath)" unpack /p "$(AdvancedPastePackagePath)" /d "$(JobOutputDirectory)/AdvancedPastePackageContents"
+ displayName: Unpack the AdvancedPaste MSIX for signing
+
+ - template: steps-esrp-signing.yml
+ parameters:
+ displayName: Sign AdvancedPaste MSIX content
+ signingIdentity: ${{ parameters.signingIdentity }}
+ inputs:
+ FolderPath: '$(JobOutputDirectory)/AdvancedPastePackageContents'
+ signType: batchSigning
+ batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_advancedpaste_msix_content.json'
+ ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
+
+ - pwsh: |-
+ $outDir = New-Item -Type Directory "$(JobOutputDirectory)/_appx" -ErrorAction:Ignore
+ $PackageFilename = Join-Path $outDir.FullName (Split-Path -Leaf "$(AdvancedPastePackagePath)")
+ & "$(MakeAppxPath)" pack /h SHA256 /o /p $PackageFilename /d "$(JobOutputDirectory)/AdvancedPastePackageContents"
+ Copy-Item -Force $PackageFilename "$(AdvancedPastePackagePath)"
+ Remove-Item -Force -Recurse "$(JobOutputDirectory)/AdvancedPastePackageContents" -ErrorAction:Ignore
+ Remove-Item -Force -Recurse "$(JobOutputDirectory)/_appx" -ErrorAction:Ignore
+ displayName: Re-pack the new AdvancedPaste package after signing
+
- pwsh: |
$testsPath = "$(Build.SourcesDirectory)/$(BuildPlatform)/$(BuildConfiguration)/tests"
if (Test-Path $testsPath) {
@@ -565,6 +631,10 @@ jobs:
Copy-Item -Verbose -Force "$(CmdPalPackagePath)" "$(JobOutputDirectory)"
displayName: Stage the final CmdPal package
+ - pwsh: |-
+ Copy-Item -Verbose -Force "$(AdvancedPastePackagePath)" "$(JobOutputDirectory)"
+ displayName: Stage the final AdvancedPaste package
+
- template: steps-build-installer-vnext.yml
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 940ff302deca..8b66248d748f 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -43,7 +43,7 @@
"name": "Run AdvancedPaste (managed, no build, ARCH configurable)",
"type": "coreclr",
"request": "launch",
- "program": "${workspaceFolder}\\${input:arch}\\Debug\\WinUI3Apps\\PowerToys.AdvancedPaste.exe",
+ "program": "${workspaceFolder}\\${input:arch}\\Debug\\WinUI3Apps\\AdvancedPaste\\PowerToys.AdvancedPaste.exe",
"args": [],
"cwd": "${workspaceFolder}",
"env": {},
diff --git a/Directory.Build.props b/Directory.Build.props
index aa2d3fc60081..2a91f0286a89 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,6 +3,7 @@
$(MSBuildThisFileDirectory)
+
Copyright (C) Microsoft Corporation. All rights reserved.
Copyright (C) Microsoft Corporation. All rights reserved.
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 6ce7261dfaa2..448262feb78d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -78,10 +78,10 @@
-
-
-
-
+
+
+
+
diff --git a/doc/devdocs/modules/advancedpaste.md b/doc/devdocs/modules/advancedpaste.md
index b2ab24443245..fec7b4eb7acb 100644
--- a/doc/devdocs/modules/advancedpaste.md
+++ b/doc/devdocs/modules/advancedpaste.md
@@ -33,7 +33,53 @@ See the `ExecutePasteFormatAsync(PasteFormat, PasteActionSource)` method in `Opt
## Debugging
-TODO: Add debugging information
+Advanced Paste is packaged as a self-contained MSIX with its own identity (`Microsoft.PowerToys.AdvancedPaste`). This gives it native package identity for Windows AI APIs (Phi Silica) and clean `ms-appx:///` resource resolution without workarounds.
+
+The MSIX is output to `WinUI3Apps/AdvancedPaste/` and registered by the module interface at runtime (or by the installer on release builds).
+
+### Running and attaching the debugger
+
+1. Set the **Runner** project (`src/runner`) as the startup project in Visual Studio.
+2. Launch the Runner (F5). This starts the PowerToys tray icon and loads all module interfaces.
+3. Open Settings (right-click tray icon → Settings) and enable the **Advanced Paste** module if it isn't already. The module launches `PowerToys.AdvancedPaste.exe` in the background immediately.
+4. In Visual Studio, go to **Debug → Attach to Process** (`Ctrl+Alt+P`) and attach to `PowerToys.AdvancedPaste.exe` (select **Managed (.NET Core)** debugger).
+
+Alternatively, use the VS Code launch configuration **"Run AdvancedPaste"** from [.vscode/launch.json](/.vscode/launch.json) to launch the exe directly — but note that without the Runner, IPC and hotkeys won't work.
+
+### MSIX package identity
+
+Advanced Paste uses the Windows AI APIs (Phi Silica / `Microsoft.Windows.AI.Text.LanguageModel`) which require **package identity** at runtime. The app is packaged as a self-contained MSIX (following the same pattern as Command Palette).
+
+#### How it works
+
+- **Build**: The csproj has `EnableMsixTooling=true` and `GenerateAppxPackageOnBuild=true` (CI builds). This produces an MSIX in `AppPackages/`.
+- **Local dev**: VS registers the package automatically via `Add-AppxPackage -Register AppxManifest.xml` when you build.
+- **Installer**: The WiX installer deploys the MSIX file and a custom action (`InstallAdvancedPastePackageCA`) registers it via `PackageManager`.
+
+#### Local development setup
+
+For local debug builds, Visual Studio handles MSIX registration automatically. Select the **"PowerToys.AdvancedPaste (Package)"** launch profile in the debug dropdown.
+
+To manually register:
+```powershell
+Add-AppxPackage -Register "ARM64\Debug\WinUI3Apps\AdvancedPaste\AppxManifest.xml"
+```
+
+Verify:
+```powershell
+$pkg = Get-AppxPackage -Name "*AdvancedPaste*"
+$pkg.Name # Microsoft.PowerToys.AdvancedPaste.Dev
+$pkg.IsDevelopmentMode # True
+```
+
+### How Settings UI checks Phi Silica availability
+
+Settings UI does not have MSIX package identity. To check whether Phi Silica is available, it queries the running Advanced Paste process via a named pipe (`powertoys_advancedpaste_phi_status`).
+
+Advanced Paste checks LAF + `GetReadyState()` once on startup (with MSIX identity), caches the result, and serves it to any client that connects. Settings connects with a 5-second timeout and reads one of:
+- `Available` — model is ready
+- `NotReady` — model needs download via Windows Update
+- `NotSupported` — not a Copilot+ PC, API unavailable, or Advanced Paste not running
## Settings
diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
index 39786a16d886..6b3b2168261a 100644
--- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
+++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
@@ -1415,6 +1415,80 @@ UINT __stdcall UnRegisterCmdPalPackageCA(MSIHANDLE hInstall)
return WcaFinalize(er);
}
+UINT __stdcall InstallAdvancedPastePackageCA(MSIHANDLE hInstall)
+{
+ using namespace winrt::Windows::Foundation;
+ using namespace winrt::Windows::Management::Deployment;
+
+ HRESULT hr = S_OK;
+ UINT er = ERROR_SUCCESS;
+ std::wstring installationFolder;
+
+ hr = WcaInitialize(hInstall, "InstallAdvancedPastePackage");
+ hr = getInstallFolder(hInstall, installationFolder);
+
+ try
+ {
+ auto msix = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\AdvancedPaste\\", false);
+ auto dependencies = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\AdvancedPaste\\Dependencies\\", true);
+
+ if (!msix.empty())
+ {
+ auto msixPath = msix[0];
+
+ if (!package::RegisterPackage(msixPath, dependencies))
+ {
+ Logger::error(L"Failed to install AdvancedPaste package");
+ er = ERROR_INSTALL_FAILURE;
+ }
+ }
+ }
+ catch (std::exception &e)
+ {
+ std::string errorMessage{"Exception thrown while trying to install AdvancedPaste package: "};
+ errorMessage += e.what();
+ Logger::error(errorMessage);
+
+ er = ERROR_INSTALL_FAILURE;
+ }
+
+ er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
+ return WcaFinalize(er);
+}
+
+UINT __stdcall UnRegisterAdvancedPastePackageCA(MSIHANDLE hInstall)
+{
+ using namespace winrt::Windows::Foundation;
+ using namespace winrt::Windows::Management::Deployment;
+
+ HRESULT hr = S_OK;
+ UINT er = ERROR_SUCCESS;
+
+ hr = WcaInitialize(hInstall, "UnRegisterAdvancedPastePackageCA");
+
+ try
+ {
+ std::wstring packageToRemoveDisplayName{L"PowerToys.AdvancedPaste"};
+
+ if (!package::UnRegisterPackage(packageToRemoveDisplayName))
+ {
+ Logger::error(L"Failed to unregister package: " + packageToRemoveDisplayName);
+ er = ERROR_INSTALL_FAILURE;
+ }
+ }
+ catch (std::exception &e)
+ {
+ std::string errorMessage{"Exception thrown while trying to unregister the AdvancedPaste package: "};
+ errorMessage += e.what();
+ Logger::error(errorMessage);
+
+ er = ERROR_INSTALL_FAILURE;
+ }
+
+ er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
+ return WcaFinalize(er);
+}
+
UINT __stdcall UnRegisterContextMenuPackagesCA(MSIHANDLE hInstall)
{
diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
index 86efe34aa6b9..8675f3513875 100644
--- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
+++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
@@ -20,8 +20,10 @@ EXPORTS
InstallEmbeddedMSIXCA
InstallDSCModuleCA
InstallCmdPalPackageCA
+ InstallAdvancedPastePackageCA
UnApplyModulesRegistryChangeSetsCA
UnRegisterCmdPalPackageCA
+ UnRegisterAdvancedPastePackageCA
UnRegisterContextMenuPackagesCA
UninstallEmbeddedMSIXCA
UninstallDSCModuleCA
diff --git a/installer/PowerToysSetupVNext/AdvancedPaste.wxs b/installer/PowerToysSetupVNext/AdvancedPaste.wxs
index f7a01b29ab5e..419ba2192eea 100644
--- a/installer/PowerToysSetupVNext/AdvancedPaste.wxs
+++ b/installer/PowerToysSetupVNext/AdvancedPaste.wxs
@@ -1,27 +1,65 @@
-
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
-
diff --git a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
index a7a9744e878b..5570320d7d31 100644
--- a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
+++ b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
@@ -4,7 +4,7 @@
false
- Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\x64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion)
@@ -17,7 +17,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
- Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\ARM64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion)
+ Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\ARM64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion);AdvancedPasteVersion=$(AdvancedPasteVersion)
IF NOT DEFINED IsPipeline (
call "$([MSBuild]::GetVsInstallRoot())\Common7\Tools\VsDevCmd.bat" -arch=arm64 -host_arch=amd64 -winsdk=10.0.19041.0 -vcvars_ver=$(VCToolsVersion)
SET PTRoot=$(SolutionDir)\..
diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs
index 584d61c4497f..1be22d6a0e3f 100644
--- a/installer/PowerToysSetupVNext/Product.wxs
+++ b/installer/PowerToysSetupVNext/Product.wxs
@@ -109,6 +109,7 @@
+
@@ -124,6 +125,7 @@
+
@@ -144,6 +146,7 @@
+
@@ -170,6 +173,8 @@
+
+
@@ -263,6 +268,10 @@
+
+
+
+
diff --git a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
index 001e0210194d..8995344b1855 100644
--- a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
+++ b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
@@ -139,8 +139,7 @@ Generate-FileList -fileDepsJson "" -fileListName WinUI3ApplicationsFiles -wxsFil
Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs
#AdvancedPaste
-Generate-FileList -fileDepsJson "" -fileListName AdvancedPasteAssetsFiles -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\AdvancedPaste"
-Generate-FileComponents -fileListName "AdvancedPasteAssetsFiles" -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs
+# AdvancedPaste ships as MSIX package (like CmdPal), not loose files.
#AwakeFiles
Generate-FileList -fileDepsJson "" -fileListName AwakeImagesFiles -wxsFilePath $PSScriptRoot\Awake.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Awake"
diff --git a/src/CmdPalVersion.props b/src/CmdPalVersion.props
index c3c5d7b6088d..69f19e3b9802 100644
--- a/src/CmdPalVersion.props
+++ b/src/CmdPalVersion.props
@@ -6,6 +6,9 @@
0.0.1.0
+ $(XES_APPXMANIFESTVERSION)
+ 0.0.1.0
+
Local
diff --git a/src/PackageIdentity/AppxManifest.xml b/src/PackageIdentity/AppxManifest.xml
index 502cc33ff00d..31acebc7c8ee 100644
--- a/src/PackageIdentity/AppxManifest.xml
+++ b/src/PackageIdentity/AppxManifest.xml
@@ -48,16 +48,6 @@
AppListEntry="none">
-
-
-
-
+
+
+
+ 6mRhg/rVL3rZ+N1LtLghtA==
+ 8wekyb3d8bbwe
+
+
diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs
index 4446e24dde29..c02bb5eb8c22 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs
@@ -55,6 +55,22 @@ public IntegrationTestUserSettings()
public IReadOnlyList AdditionalActions => _additionalActions;
+ public string FixSpellingAndGrammarPrompt => string.Empty;
+
+ public string FixSpellingAndGrammarSystemPrompt => string.Empty;
+
+ public string FixSpellingAndGrammarProviderId => string.Empty;
+
+ public bool FixSpellingAndGrammarCoachingEnabled => false;
+
+ public bool FixSpellingAndGrammarCoachingShortcutSet => false;
+
+ public string FixSpellingAndGrammarCoachingPrompt => string.Empty;
+
+ public string FixSpellingAndGrammarCoachingSystemPrompt => string.Empty;
+
+ public string FixSpellingAndGrammarCoachingProviderId => string.Empty;
+
public PasteAIConfiguration PasteAIConfiguration => _configuration;
public event EventHandler Changed;
diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/CustomActionKernelQueryCacheServiceTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/CustomActionKernelQueryCacheServiceTests.cs
index b93fda488443..1994da0af086 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/CustomActionKernelQueryCacheServiceTests.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/CustomActionKernelQueryCacheServiceTests.cs
@@ -41,7 +41,7 @@ public void TestInitialize()
UpdateUserActions([], []);
_fileSystem = new();
- _cacheService = new(_userSettings.Object, _fileSystem);
+ _cacheService = new(_userSettings.Object, _fileSystem, resourceId => resourceId);
}
[TestMethod]
@@ -122,7 +122,7 @@ public async Task Test_Cache_Is_Persistent()
await _cacheService.WriteAsync(JSONTestKey, TestValue);
await _cacheService.WriteAsync(MarkdownTestKey, TestValue2);
- _cacheService = new(_userSettings.Object, _fileSystem); // recreate using same mock file-system to simulate app restart
+ _cacheService = new(_userSettings.Object, _fileSystem, resourceId => resourceId); // recreate using same mock file-system to simulate app restart
AssertAreEqual(TestValue, _cacheService.ReadOrNull(JSONTestKey));
AssertAreEqual(TestValue2, _cacheService.ReadOrNull(MarkdownTestKey));
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj
index 090f3c75a7f5..63faecaa844f 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj
@@ -1,30 +1,41 @@
+
WinExe
- $(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps
+ $(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps\AdvancedPaste
true
Assets\AdvancedPaste\AdvancedPaste.ico
app.manifest
true
false
false
- true
- None
+ true
true
PowerToys.AdvancedPaste
PowerToys AdvancedPaste
AdvancedPaste
true
true
-
- PowerToys.AdvancedPaste.pri
DISABLE_XAML_GENERATED_MAIN,TRACE
+ $(AdvancedPasteVersion)
+
+ true
+ Never
+ PowerToys.AdvancedPaste
+ $(OutputPath)\AppPackages\PowerToys.AdvancedPaste_$(Version)_Test\
+
+
+
+
+
+
+
PowerToys.GPOWrapper
@@ -32,6 +43,25 @@
false
+
+
+
+ $(ApplicationManifest.Replace("$(MSBuildProjectDirectory)\",""))
+
+
+
+
+
+
+
+ $(IntermediateOutputPath)PhiSilicaLafCredentials.g.cs
+
+
+
+
+
+
+
@@ -66,9 +96,9 @@
+
-
@@ -76,11 +106,6 @@
-
-
-
-
-
@@ -99,6 +124,10 @@
+
+
+
+
<_Parameter1>AdvancedPaste.UnitTests
@@ -106,8 +135,6 @@
-
-
@@ -156,4 +183,5 @@
PreserveNewest
+
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs
index 3fa940952ecd..4299219e9609 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs
@@ -90,6 +90,10 @@ public App()
viewModel = GetService();
UnhandledException += App_UnhandledException;
+
+ // Start a background pipe server so Settings UI can query Phi Silica status.
+ // This runs with MSIX package identity, so LAF + GetReadyState() work.
+ StartPhiSilicaStatusServer();
}
public MainWindow GetMainWindow()
@@ -108,12 +112,111 @@ public static T GetService()
return service;
}
+ ///
+ /// Starts a background named pipe server that responds to Phi Silica status queries
+ /// from Settings UI. The pipe runs for the lifetime of the app.
+ ///
+ private static void StartPhiSilicaStatusServer()
+ {
+ Task.Run(async () =>
+ {
+ // Check status once (with MSIX identity) and cache
+ string status;
+ try
+ {
+ PhiSilicaLafHelper.TryUnlock();
+ var readyState = Microsoft.Windows.AI.Text.LanguageModel.GetReadyState();
+ status = readyState switch
+ {
+ Microsoft.Windows.AI.AIFeatureReadyState.NotSupportedOnCurrentSystem => "NotSupported",
+ Microsoft.Windows.AI.AIFeatureReadyState.NotReady => "NotReady",
+ _ => "Available",
+ };
+ }
+ catch
+ {
+ status = "NotSupported";
+ }
+
+ Logger.LogDebug($"Phi Silica status: {status}");
+
+ // Serve status to any client that connects
+ while (true)
+ {
+ try
+ {
+ using var server = new System.IO.Pipes.NamedPipeServerStream(
+ "powertoys_advancedpaste_phi_status",
+ System.IO.Pipes.PipeDirection.Out,
+ 1,
+ System.IO.Pipes.PipeTransmissionMode.Byte,
+ System.IO.Pipes.PipeOptions.Asynchronous);
+
+ await server.WaitForConnectionAsync();
+
+ var bytes = System.Text.Encoding.UTF8.GetBytes(status);
+ await server.WriteAsync(bytes);
+ server.WaitForPipeDrain();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Phi Silica status pipe error", ex);
+ await Task.Delay(1000);
+ }
+ }
+ });
+ }
+
///
/// Invoked when the application is launched.
///
/// Details about the launch request and process.
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
+ // Try protocol activation args first (MSIX launch via x-advancedpaste:// URI)
+ try
+ {
+ var activatedArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs();
+ Logger.LogDebug($"Activation kind: {activatedArgs?.Kind}");
+
+ if (activatedArgs?.Kind == Microsoft.Windows.AppLifecycle.ExtendedActivationKind.Protocol &&
+ activatedArgs.Data is Windows.ApplicationModel.Activation.IProtocolActivatedEventArgs protocolArgs)
+ {
+ var uri = protocolArgs.Uri;
+ Logger.LogDebug($"Protocol URI: {uri}");
+
+ // Parse query parameters manually to avoid System.Web dependency
+ var pid = GetQueryParam(uri.Query, "pid");
+ var pipeName = GetQueryParam(uri.Query, "pipe");
+
+ Logger.LogDebug($"Parsed pid={pid}, pipe={pipeName}");
+
+ if (int.TryParse(pid, out int powerToysRunnerPid))
+ {
+ RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () =>
+ {
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ Dispose();
+ Environment.Exit(0);
+ });
+ });
+ }
+
+ if (!string.IsNullOrEmpty(pipeName))
+ {
+ ProcessNamedPipe(pipeName);
+ }
+
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to process protocol activation args", ex);
+ }
+
+ // Fallback: command-line args (direct exe launch)
var cmdArgs = Environment.GetCommandLineArgs();
if (cmdArgs?.Length > 1)
{
@@ -138,11 +241,33 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
private void ProcessNamedPipe(string pipeName)
{
+ Logger.LogDebug($"Connecting to named pipe: {pipeName}");
void OnMessage(string message) => _dispatcherQueue.TryEnqueue(async () => await OnNamedPipeMessage(message));
Task.Run(async () => await NamedPipeProcessor.ProcessNamedPipeAsync(pipeName, connectTimeout: TimeSpan.FromSeconds(10), OnMessage, CancellationToken.None));
}
+ private static string GetQueryParam(string query, string key)
+ {
+ if (string.IsNullOrEmpty(query))
+ {
+ return null;
+ }
+
+ // Remove leading '?' if present
+ var q = query.StartsWith('?') ? query.Substring(1) : query;
+ foreach (var part in q.Split('&'))
+ {
+ var kv = part.Split('=', 2);
+ if (kv.Length == 2 && kv[0].Equals(key, StringComparison.OrdinalIgnoreCase))
+ {
+ return Uri.UnescapeDataString(kv[1]);
+ }
+ }
+
+ return null;
+ }
+
private async Task OnNamedPipeMessage(string message)
{
var messageParts = message.Split();
@@ -188,14 +313,23 @@ private async Task OnAdvancedPasteAdditionalActionHotkey(string[] messageParts)
}
else
{
- if (!AdditionalActionIPCKeys.TryGetValue(messageParts[1], out PasteFormats pasteFormat))
+ const string coachingSuffix = "-coaching";
+ var actionKey = messageParts[1];
+ bool forceCoaching = actionKey.EndsWith(coachingSuffix, StringComparison.OrdinalIgnoreCase);
+
+ if (forceCoaching)
+ {
+ actionKey = actionKey[..^coachingSuffix.Length];
+ }
+
+ if (!AdditionalActionIPCKeys.TryGetValue(actionKey, out PasteFormats pasteFormat))
{
Logger.LogWarning($"Unexpected additional action type {messageParts[1]}");
}
else
{
await ShowWindow();
- await viewModel.ExecutePasteFormatAsync(pasteFormat, PasteActionSource.GlobalKeyboardShortcut);
+ await viewModel.ExecutePasteFormatAsync(pasteFormat, PasteActionSource.GlobalKeyboardShortcut, forceCoaching);
}
}
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml
index 6303564d9b92..254edd26c2a8 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml
@@ -382,6 +382,7 @@
+
-
+
+
+
+
+
+
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
index d692263dc167..afa958faf83a 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
@@ -25,6 +25,22 @@ public interface IUserSettings
public IReadOnlyList AdditionalActions { get; }
+ public string FixSpellingAndGrammarPrompt { get; }
+
+ public string FixSpellingAndGrammarSystemPrompt { get; }
+
+ public string FixSpellingAndGrammarProviderId { get; }
+
+ public bool FixSpellingAndGrammarCoachingEnabled { get; }
+
+ public bool FixSpellingAndGrammarCoachingShortcutSet { get; }
+
+ public string FixSpellingAndGrammarCoachingPrompt { get; }
+
+ public string FixSpellingAndGrammarCoachingSystemPrompt { get; }
+
+ public string FixSpellingAndGrammarCoachingProviderId { get; }
+
public PasteAIConfiguration PasteAIConfiguration { get; }
public event EventHandler Changed;
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/NativeMethods.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/NativeMethods.cs
index 08293d4be078..0074164242ff 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/NativeMethods.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/NativeMethods.cs
@@ -157,8 +157,6 @@ internal struct PointInter
{
public int X;
public int Y;
-
- public static explicit operator System.Windows.Point(PointInter point) => new System.Windows.Point(point.X, point.Y);
}
[DllImport("user32.dll")]
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ResourceLoaderInstance.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ResourceLoaderInstance.cs
index 70085ee31420..6bc81aae5a70 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ResourceLoaderInstance.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ResourceLoaderInstance.cs
@@ -11,7 +11,7 @@ internal static class ResourceLoaderInstance
static ResourceLoaderInstance()
{
- ResourceLoader = new ResourceLoader("PowerToys.AdvancedPaste.pri");
+ ResourceLoader = new ResourceLoader();
}
}
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
index 59f31f0e99c7..4c7622cf74b3 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
@@ -46,6 +46,22 @@ internal sealed partial class UserSettings : IUserSettings, IDisposable
public IReadOnlyList CustomActions => _customActions;
+ public string FixSpellingAndGrammarPrompt { get; private set; } = string.Empty;
+
+ public string FixSpellingAndGrammarSystemPrompt { get; private set; } = string.Empty;
+
+ public string FixSpellingAndGrammarProviderId { get; private set; } = string.Empty;
+
+ public bool FixSpellingAndGrammarCoachingEnabled { get; private set; }
+
+ public bool FixSpellingAndGrammarCoachingShortcutSet { get; private set; }
+
+ public string FixSpellingAndGrammarCoachingPrompt { get; private set; } = string.Empty;
+
+ public string FixSpellingAndGrammarCoachingSystemPrompt { get; private set; } = string.Empty;
+
+ public string FixSpellingAndGrammarCoachingProviderId { get; private set; } = string.Empty;
+
public PasteAIConfiguration PasteAIConfiguration { get; private set; }
public UserSettings(IFileSystem fileSystem)
@@ -113,10 +129,21 @@ void UpdateSettings()
EnableClipboardPreview = properties.EnableClipboardPreview;
PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();
+ var fixSpellingAction = properties.AdditionalActions.FixSpellingAndGrammar;
+ FixSpellingAndGrammarPrompt = fixSpellingAction.Prompt ?? string.Empty;
+ FixSpellingAndGrammarSystemPrompt = fixSpellingAction.SystemPrompt ?? string.Empty;
+ FixSpellingAndGrammarProviderId = fixSpellingAction.ProviderId ?? string.Empty;
+ FixSpellingAndGrammarCoachingEnabled = fixSpellingAction.CoachingEnabled;
+ FixSpellingAndGrammarCoachingShortcutSet = fixSpellingAction.CoachingShortcut?.Code > 0;
+ FixSpellingAndGrammarCoachingPrompt = fixSpellingAction.CoachingPrompt ?? string.Empty;
+ FixSpellingAndGrammarCoachingSystemPrompt = fixSpellingAction.CoachingSystemPrompt ?? string.Empty;
+ FixSpellingAndGrammarCoachingProviderId = fixSpellingAction.CoachingProviderId ?? string.Empty;
+
var sourceAdditionalActions = properties.AdditionalActions;
(PasteFormats Format, IAdvancedPasteAction[] Actions)[] additionalActionFormats =
[
(PasteFormats.ImageToText, [sourceAdditionalActions.ImageToText]),
+ (PasteFormats.FixSpellingAndGrammar, [sourceAdditionalActions.FixSpellingAndGrammar]),
(PasteFormats.PasteAsTxtFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsTxtFile]),
(PasteFormats.PasteAsPngFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsPngFile]),
(PasteFormats.PasteAsHtmlFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsHtmlFile]),
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs
index e1df90897e35..da956ffeed5c 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs
@@ -24,20 +24,22 @@ private PasteFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool
IsEnabled = SupportsClipboardFormats(clipboardFormats) && (isAIServiceEnabled || !Metadata.RequiresAIService);
}
- public static PasteFormat CreateStandardFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, Func resourceLoader) =>
+ public static PasteFormat CreateStandardFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, Func resourceLoader, string providerId = null) =>
new(format, clipboardFormats, isAIServiceEnabled)
{
Name = MetadataDict[format].ResourceId == null ? string.Empty : resourceLoader(MetadataDict[format].ResourceId),
Prompt = string.Empty,
IsSavedQuery = false,
+ ProviderId = providerId ?? string.Empty,
};
- public static PasteFormat CreateCustomAIFormat(PasteFormats format, string name, string prompt, bool isSavedQuery, ClipboardFormat clipboardFormats, bool isAIServiceEnabled) =>
+ public static PasteFormat CreateCustomAIFormat(PasteFormats format, string name, string prompt, bool isSavedQuery, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, string providerId = null) =>
new(format, clipboardFormats, isAIServiceEnabled)
{
Name = name,
Prompt = prompt,
IsSavedQuery = isSavedQuery,
+ ProviderId = providerId ?? string.Empty,
};
public PasteFormatMetadataAttribute Metadata => MetadataDict[Format];
@@ -50,6 +52,8 @@ public static PasteFormat CreateCustomAIFormat(PasteFormats format, string name,
public string Prompt { get; private init; }
+ public string ProviderId { get; private init; } = string.Empty;
+
public bool IsSavedQuery { get; private init; }
public bool IsEnabled { get; private init; }
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs
index 1479912e66f7..8b06128798f4 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs
@@ -38,6 +38,17 @@ public enum PasteFormats
KernelFunctionDescription = "Takes clipboard text and formats it as JSON text.")]
Json,
+ [PasteFormatMetadata(
+ IsCoreAction = false,
+ ResourceId = "FixSpellingAndGrammar",
+ IconGlyph = "\uE8E2",
+ RequiresAIService = true,
+ CanPreview = true,
+ SupportedClipboardFormats = ClipboardFormat.Text,
+ IPCKey = AdvancedPasteAdditionalActions.PropertyNames.FixSpellingAndGrammar,
+ KernelFunctionDescription = "Fixes all spelling and grammar errors in the clipboard text and returns the corrected version.")]
+ FixSpellingAndGrammar,
+
[PasteFormatMetadata(
IsCoreAction = false,
ResourceId = "ImageToText",
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Package-Dev.appxmanifest b/src/modules/AdvancedPaste/AdvancedPaste/Package-Dev.appxmanifest
new file mode 100644
index 000000000000..aafe865166d3
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Package-Dev.appxmanifest
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+ PowerToys Advanced Paste (Dev)
+ A Lone Developer
+ Assets\AdvancedPaste\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PowerToys Advanced Paste (Dev)
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Package.appxmanifest b/src/modules/AdvancedPaste/AdvancedPaste/Package.appxmanifest
new file mode 100644
index 000000000000..287e0788e9c1
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Package.appxmanifest
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+ PowerToys Advanced Paste
+ Microsoft Corporation
+ Assets\AdvancedPaste\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PowerToys Advanced Paste
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/PhiSilicaLafHelper.cs b/src/modules/AdvancedPaste/AdvancedPaste/PhiSilicaLafHelper.cs
new file mode 100644
index 000000000000..25714688ea3f
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/PhiSilicaLafHelper.cs
@@ -0,0 +1,56 @@
+// 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 System;
+using System.Diagnostics;
+using Windows.ApplicationModel;
+
+namespace AdvancedPaste;
+
+internal static class PhiSilicaLafHelper
+{
+ private const string FeatureId = "com.microsoft.windows.ai.languagemodel";
+
+ private static readonly object _lock = new();
+ private static bool _attempted;
+ private static bool _unlocked;
+
+ public static bool TryUnlock()
+ {
+ if (_attempted)
+ {
+ return _unlocked;
+ }
+
+ lock (_lock)
+ {
+ if (_attempted)
+ {
+ return _unlocked;
+ }
+
+ _attempted = true;
+
+ try
+ {
+ var access = LimitedAccessFeatures.TryUnlockFeature(
+ FeatureId,
+ PhiSilicaLafCredentials.Token,
+ PhiSilicaLafCredentials.Attestation + " has registered their use of com.microsoft.windows.ai.languagemodel with Microsoft and agrees to the terms of use.");
+
+ _unlocked = access.Status == LimitedAccessFeatureStatus.Available
+ || access.Status == LimitedAccessFeatureStatus.AvailableWithoutToken;
+
+ Debug.WriteLine($"Phi Silica LAF unlock status: {access.Status}");
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Phi Silica LAF unlock failed: {ex.Message}");
+ _unlocked = false;
+ }
+
+ return _unlocked;
+ }
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Program.cs b/src/modules/AdvancedPaste/AdvancedPaste/Program.cs
index ef089f9511ff..ab5e91f450b2 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Program.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Program.cs
@@ -14,7 +14,7 @@ namespace AdvancedPaste
public static class Program
{
[STAThread]
- public static void Main(string[] args)
+ public static int Main(string[] args)
{
Logger.InitializeLogger("\\AdvancedPaste\\Logs");
@@ -23,7 +23,7 @@ public static void Main(string[] args)
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAdvancedPasteEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
- return;
+ return 1;
}
var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_AdvancedPaste_Instance");
@@ -41,6 +41,8 @@ public static void Main(string[] args)
{
Logger.LogWarning("Another instance of AdvancedPasteUI is running. Exiting.");
}
+
+ return 0;
}
}
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActionKernelQueryCacheService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActionKernelQueryCacheService.cs
index f7d888cf10f5..3c06bb56155f 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActionKernelQueryCacheService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActionKernelQueryCacheService.cs
@@ -33,14 +33,21 @@ public sealed class CustomActionKernelQueryCacheService : IKernelQueryCacheServi
private readonly IUserSettings _userSettings;
private readonly IFileSystem _fileSystem;
private readonly SettingsUtils _settingsUtil;
+ private readonly Func _getLocalizedString;
private static string Version => Assembly.GetExecutingAssembly()?.GetName()?.Version?.ToString() ?? string.Empty;
public CustomActionKernelQueryCacheService(IUserSettings userSettings, IFileSystem fileSystem)
+ : this(userSettings, fileSystem, ResourceLoaderInstance.ResourceLoader.GetString)
+ {
+ }
+
+ internal CustomActionKernelQueryCacheService(IUserSettings userSettings, IFileSystem fileSystem, Func getLocalizedString)
{
_userSettings = userSettings;
_fileSystem = fileSystem;
_settingsUtil = new SettingsUtils(fileSystem);
+ _getLocalizedString = getLocalizedString;
_userSettings.Changed += OnUserSettingsChanged;
@@ -112,7 +119,7 @@ private void UpdateCacheablePrompts()
let metadata = pair.Value
where !string.IsNullOrEmpty(metadata.ResourceId)
where metadata.IsCoreAction || _userSettings.AdditionalActions.Contains(format)
- select ResourceLoaderInstance.ResourceLoader.GetString(metadata.ResourceId);
+ select _getLocalizedString(metadata.ResourceId);
var customActionPrompts = from customAction in _userSettings.CustomActions
select customAction.Prompt;
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs
index 05cdcbe81fa0..3cefc56b4c81 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs
@@ -40,10 +40,15 @@ public CustomActionTransformService(IPromptModerationService promptModerationSer
this.userSettings = userSettings;
}
- public async Task TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress progress)
+ public async Task TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress progress, string systemPromptOverride = null, string providerIdOverride = null)
{
var pasteConfig = userSettings?.PasteAIConfiguration;
- var providerConfig = BuildProviderConfig(pasteConfig);
+ var providerConfig = BuildProviderConfig(pasteConfig, providerIdOverride);
+
+ if (systemPromptOverride != null)
+ {
+ providerConfig.SystemPrompt = systemPromptOverride;
+ }
return await TransformAsync(prompt, inputText, imageBytes, providerConfig, cancellationToken, progress);
}
@@ -148,13 +153,26 @@ private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
}
- private PasteAIConfig BuildProviderConfig(PasteAIConfiguration config)
+ private PasteAIConfig BuildProviderConfig(PasteAIConfiguration config, string providerIdOverride = null)
{
config ??= new PasteAIConfiguration();
- var provider = config.ActiveProvider ?? config.Providers?.FirstOrDefault() ?? new PasteAIProviderDefinition();
+ PasteAIProviderDefinition provider;
+
+ if (!string.IsNullOrWhiteSpace(providerIdOverride))
+ {
+ provider = config.Providers?.FirstOrDefault(p => string.Equals(p.Id, providerIdOverride, StringComparison.OrdinalIgnoreCase))
+ ?? config.ActiveProvider
+ ?? config.Providers?.FirstOrDefault()
+ ?? new PasteAIProviderDefinition();
+ }
+ else
+ {
+ provider = config.ActiveProvider ?? config.Providers?.FirstOrDefault() ?? new PasteAIProviderDefinition();
+ }
+
var serviceType = NormalizeServiceType(provider.ServiceTypeKind);
var systemPrompt = string.IsNullOrWhiteSpace(provider.SystemPrompt) ? DefaultSystemPrompt : provider.SystemPrompt;
- var apiKey = AcquireApiKey(serviceType);
+ var apiKey = AcquireApiKey(serviceType, provider.Id);
var modelName = provider.ModelName;
var providerConfig = new PasteAIConfig
@@ -173,15 +191,14 @@ private PasteAIConfig BuildProviderConfig(PasteAIConfiguration config)
return providerConfig;
}
- private string AcquireApiKey(AIServiceType serviceType)
+ private string AcquireApiKey(AIServiceType serviceType, string providerId)
{
if (!RequiresApiKey(serviceType))
{
return string.Empty;
}
- credentialsProvider.Refresh();
- return credentialsProvider.GetKey() ?? string.Empty;
+ return credentialsProvider.GetKey(serviceType, providerId ?? string.Empty);
}
private static bool RequiresApiKey(AIServiceType serviceType)
@@ -190,6 +207,8 @@ private static bool RequiresApiKey(AIServiceType serviceType)
{
AIServiceType.Onnx => false,
AIServiceType.Ollama => false,
+ AIServiceType.FoundryLocal => false,
+ AIServiceType.PhiSilica => false,
_ => true,
};
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs
index 564db3fdc56e..361d96d4c7a3 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs
@@ -12,6 +12,6 @@ namespace AdvancedPaste.Services.CustomActions
{
public interface ICustomActionTransformService
{
- Task TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress progress);
+ Task TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress progress, string systemPromptOverride = null, string providerIdOverride = null);
}
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderFactory.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderFactory.cs
index 7339b4e4e38f..4f7e02fdc303 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderFactory.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderFactory.cs
@@ -15,6 +15,7 @@ public sealed class PasteAIProviderFactory : IPasteAIProviderFactory
SemanticKernelPasteProvider.Registration,
LocalModelPasteProvider.Registration,
FoundryLocalPasteProvider.Registration,
+ PhiSilicaPasteProvider.Registration,
};
private static readonly IReadOnlyDictionary> ProviderFactories = CreateProviderFactories();
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PhiSilicaPasteProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PhiSilicaPasteProvider.cs
new file mode 100644
index 000000000000..ef87c204f706
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PhiSilicaPasteProvider.cs
@@ -0,0 +1,208 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using AdvancedPaste.Models;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.Windows.AI;
+using Microsoft.Windows.AI.ContentSafety;
+using Microsoft.Windows.AI.Text;
+using PhiSilicaLanguageModel = Microsoft.Windows.AI.Text.LanguageModel;
+
+namespace AdvancedPaste.Services.CustomActions;
+
+public sealed class PhiSilicaPasteProvider : IPasteAIProvider
+{
+ private static readonly IReadOnlyCollection SupportedTypes = new[]
+ {
+ AIServiceType.PhiSilica,
+ };
+
+ public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new PhiSilicaPasteProvider(config));
+
+ private static readonly SemaphoreSlim _initLock = new(1, 1);
+ private static PhiSilicaLanguageModel _cachedModel;
+
+ private readonly PasteAIConfig _config;
+
+ public PhiSilicaPasteProvider(PasteAIConfig config)
+ {
+ ArgumentNullException.ThrowIfNull(config);
+ _config = config;
+ }
+
+ public Task IsAvailableAsync(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ PhiSilicaLafHelper.TryUnlock();
+ var readyState = PhiSilicaLanguageModel.GetReadyState();
+ return Task.FromResult(readyState != AIFeatureReadyState.NotSupportedOnCurrentSystem);
+ }
+ catch (Exception)
+ {
+ return Task.FromResult(false);
+ }
+ }
+
+ public async Task ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress progress)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+
+ try
+ {
+ var systemPrompt = request.SystemPrompt;
+ if (string.IsNullOrWhiteSpace(systemPrompt))
+ {
+ throw new PasteActionException(
+ "System prompt is required for Phi Silica",
+ new ArgumentException("System prompt must be provided", nameof(request)));
+ }
+
+ var prompt = request.Prompt;
+ var inputText = request.InputText;
+ if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
+ {
+ throw new PasteActionException(
+ "Prompt and input text are required",
+ new ArgumentException("Prompt and input text must be provided", nameof(request)));
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var languageModel = await GetOrCreateModelAsync(cancellationToken).ConfigureAwait(false);
+
+ progress?.Report(0.1);
+
+ var contentFilterOptions = new ContentFilterOptions();
+ var context = languageModel.CreateContext(systemPrompt, contentFilterOptions);
+
+ var userPrompt = $"""
+ User instructions:
+ {prompt}
+
+ Text:
+ {inputText}
+
+ Output:
+ """;
+
+ if ((ulong)userPrompt.Length > languageModel.GetUsablePromptLength(context, userPrompt))
+ {
+ throw new PasteActionException(
+ "Prompt is too large for the Phi Silica model context",
+ new InvalidOperationException("Prompt exceeds usable prompt length"),
+ aiServiceMessage: "The input text is too large for on-device processing. Try with shorter text.");
+ }
+
+ var options = new LanguageModelOptions
+ {
+ ContentFilterOptions = contentFilterOptions,
+ };
+
+ var result = await languageModel.GenerateResponseAsync(context, userPrompt, options).AsTask(cancellationToken).ConfigureAwait(false);
+
+ progress?.Report(0.8);
+
+ if (result.Status != LanguageModelResponseStatus.Complete)
+ {
+ var statusMessage = result.Status switch
+ {
+ LanguageModelResponseStatus.BlockedByPolicy => "Response was blocked by policy.",
+ LanguageModelResponseStatus.PromptBlockedByContentModeration => "Prompt was blocked by content moderation.",
+ LanguageModelResponseStatus.ResponseBlockedByContentModeration => "Response was blocked by content moderation.",
+ LanguageModelResponseStatus.PromptLargerThanContext => "Prompt is too large for the model context.",
+ _ => $"Unexpected status: {result.Status}",
+ };
+
+ throw new PasteActionException(
+ $"Phi Silica returned status: {result.Status}",
+ new InvalidOperationException($"LanguageModel response status: {result.Status}"),
+ aiServiceMessage: statusMessage);
+ }
+
+ var responseText = result.Text ?? string.Empty;
+ request.Usage = AIServiceUsage.None;
+
+ progress?.Report(1.0);
+
+ return responseText;
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (PasteActionException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw new PasteActionException(
+ "Failed to generate response using Phi Silica",
+ ex,
+ aiServiceMessage: $"Error details: {ex.Message}");
+ }
+ }
+
+ private static async Task GetOrCreateModelAsync(CancellationToken cancellationToken)
+ {
+ if (_cachedModel is not null)
+ {
+ return _cachedModel;
+ }
+
+ await _initLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ if (_cachedModel is not null)
+ {
+ return _cachedModel;
+ }
+
+ PhiSilicaLafHelper.TryUnlock();
+ var readyState = PhiSilicaLanguageModel.GetReadyState();
+
+ if (readyState is AIFeatureReadyState.NotSupportedOnCurrentSystem or AIFeatureReadyState.DisabledByUser)
+ {
+ throw new PasteActionException(
+ "Phi Silica is not supported on this device. A Copilot+ PC is required.",
+ new InvalidOperationException("Phi Silica requires a Copilot+ PC with an NPU."),
+ aiServiceMessage: "Phi Silica requires a Copilot+ PC with an NPU. For on-device AI on any Windows PC, consider using Foundry Local.");
+ }
+
+ if (readyState is AIFeatureReadyState.NotReady)
+ {
+ var ensureResult = await PhiSilicaLanguageModel.EnsureReadyAsync().AsTask(cancellationToken).ConfigureAwait(false);
+ if (ensureResult.Status != AIFeatureReadyResultState.Success)
+ {
+ throw new PasteActionException(
+ "Failed to prepare Phi Silica model",
+ ensureResult.ExtendedError,
+ aiServiceMessage: $"Model preparation failed (status: {ensureResult.Status})");
+ }
+ }
+
+ if (PhiSilicaLanguageModel.GetReadyState() is not AIFeatureReadyState.Ready)
+ {
+ throw new PasteActionException(
+ "Phi Silica model is not ready",
+ new InvalidOperationException("Phi Silica model is not in Ready state after preparation."),
+ aiServiceMessage: "Phi Silica model is not available. Please ensure the model is downloaded and ready.");
+ }
+
+ _cachedModel = await PhiSilicaLanguageModel.CreateAsync().AsTask(cancellationToken).ConfigureAwait(false);
+ return _cachedModel;
+ }
+ finally
+ {
+ _initLock.Release();
+ }
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/SemanticKernelPasteProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/SemanticKernelPasteProvider.cs
index 636d2e3e78b3..90c8d58b1bcc 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/SemanticKernelPasteProvider.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/SemanticKernelPasteProvider.cs
@@ -175,6 +175,7 @@ private PromptExecutionSettings CreateExecutionSettings()
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = null,
+ ReasoningEffort = "minimal",
},
_ => new PromptExecutionSettings(),
};
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs
index 648881fba04b..27bf71092842 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs
@@ -55,6 +55,13 @@ public bool IsConfigured()
return !string.IsNullOrEmpty(GetKey());
}
+ public string GetKey(AIServiceType serviceType, string providerId)
+ {
+ var normalizedType = NormalizeServiceType(serviceType);
+ var entry = BuildCredentialEntry(normalizedType, providerId ?? string.Empty);
+ return LoadKey(entry);
+ }
+
public bool Refresh()
{
using (_syncRoot.EnterScope())
@@ -121,6 +128,7 @@ private static string LoadKey((string Resource, string Username)? entry)
try
{
var credential = new PasswordVault().Retrieve(entry.Value.Resource, entry.Value.Username);
+ credential?.RetrievePassword();
return credential?.Password ?? string.Empty;
}
catch (Exception)
@@ -160,6 +168,7 @@ private static (string Resource, string Username)? BuildCredentialEntry(AIServic
case AIServiceType.ML:
case AIServiceType.Onnx:
case AIServiceType.Ollama:
+ case AIServiceType.PhiSilica:
return null;
default:
return null;
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs
index 7aa6f63b198a..db9739697b43 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs
@@ -2,6 +2,8 @@
// 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.PowerToys.Settings.UI.Library;
+
namespace AdvancedPaste.Services;
///
@@ -21,6 +23,14 @@ public interface IAICredentialsProvider
/// Credential string or when missing.
string GetKey();
+ ///
+ /// Retrieves the credential for a specific AI provider.
+ ///
+ /// The AI service type.
+ /// The provider identifier.
+ /// Credential string or when missing.
+ string GetKey(AIServiceType serviceType, string providerId);
+
///
/// Refreshes the cached credential for the active AI provider.
///
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelService.cs
index beb62fb293d2..6cef3b120836 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelService.cs
@@ -12,5 +12,5 @@ namespace AdvancedPaste.Services;
public interface IKernelService
{
- Task TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress progress);
+ Task TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress progress, string providerIdOverride = null);
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs
index 0d753d1ec327..392c84ee6e31 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs
@@ -46,7 +46,7 @@ public abstract class KernelServiceBase(
protected abstract IKernelRuntimeConfiguration GetRuntimeConfiguration();
- public async Task TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress progress)
+ public async Task TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress progress, string providerIdOverride = null)
{
Logger.LogTrace();
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs
index ff64a5ad8328..0f02efa77eba 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs
@@ -9,15 +9,18 @@
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Services.CustomActions;
+using AdvancedPaste.Settings;
+using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using Windows.ApplicationModel.DataTransfer;
namespace AdvancedPaste.Services;
-public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomActionTransformService customActionTransformService) : IPasteFormatExecutor
+public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomActionTransformService customActionTransformService, IUserSettings userSettings) : IPasteFormatExecutor
{
private readonly IKernelService _kernelService = kernelService;
private readonly ICustomActionTransformService _customActionTransformService = customActionTransformService;
+ private readonly IUserSettings _userSettings = userSettings;
public async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress progress)
{
@@ -36,8 +39,9 @@ public async Task ExecutePasteFormatAsync(PasteFormat pasteFormat,
return await Task.Run(async () =>
pasteFormat.Format switch
{
- PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
- PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(pasteFormat.Prompt, await clipboardData.GetTextOrHtmlTextAsync(), await clipboardData.GetImageAsPngBytesAsync(), cancellationToken, progress))?.Content ?? string.Empty),
+ PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress, pasteFormat.ProviderId),
+ PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(pasteFormat.Prompt, await clipboardData.GetTextOrHtmlTextAsync(), await clipboardData.GetImageAsPngBytesAsync(), cancellationToken, progress, providerIdOverride: pasteFormat.ProviderId))?.Content ?? string.Empty),
+ PasteFormats.FixSpellingAndGrammar => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(GetFixSpellingPrompt(), await clipboardData.GetTextOrHtmlTextAsync(), null, cancellationToken, progress, GetFixSpellingSystemPrompt(), pasteFormat.ProviderId))?.Content ?? string.Empty),
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
});
}
@@ -62,4 +66,16 @@ private static void WriteTelemetry(PasteFormats format, PasteActionSource source
throw new ArgumentOutOfRangeException(nameof(format));
}
}
+
+ private string GetFixSpellingPrompt()
+ {
+ var customPrompt = _userSettings.FixSpellingAndGrammarPrompt;
+ return string.IsNullOrWhiteSpace(customPrompt) ? AdvancedPasteDefaultPrompts.FixSpellingAndGrammar : customPrompt;
+ }
+
+ private string GetFixSpellingSystemPrompt()
+ {
+ var customSystemPrompt = _userSettings.FixSpellingAndGrammarSystemPrompt;
+ return string.IsNullOrWhiteSpace(customSystemPrompt) ? AdvancedPasteDefaultPrompts.FixSpellingAndGrammarSystem : customSystemPrompt;
+ }
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw
index f36577832131..388001b94d46 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw
@@ -232,6 +232,12 @@
Paste as plain text
+
+ Fix spelling and grammar
+
+
+ What was changed and why
+
Image to text
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
index b474b8215af6..6b704d57a86a 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
@@ -16,8 +16,8 @@
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Services;
+using AdvancedPaste.Services.CustomActions;
using AdvancedPaste.Settings;
-using Common.UI;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
@@ -41,6 +41,7 @@ public sealed partial class OptionsViewModel : ObservableObject, IProgress
@@ -341,11 +343,21 @@ private void EnqueueRefreshPasteFormats()
});
}
- private PasteFormat CreateStandardPasteFormat(PasteFormats format) =>
- PasteFormat.CreateStandardFormat(format, AvailableClipboardFormats, IsCustomAIServiceEnabled, ResourceLoaderInstance.ResourceLoader.GetString);
+ private PasteFormat CreateStandardPasteFormat(PasteFormats format)
+ {
+ var providerId = GetProviderIdForFormat(format);
+ return PasteFormat.CreateStandardFormat(format, AvailableClipboardFormats, IsCustomAIServiceEnabled, ResourceLoaderInstance.ResourceLoader.GetString, providerId);
+ }
- private PasteFormat CreateCustomAIPasteFormat(string name, string prompt, bool isSavedQuery) =>
- PasteFormat.CreateCustomAIFormat(CustomAIFormat, name, prompt, isSavedQuery, AvailableClipboardFormats, IsCustomAIServiceEnabled);
+ private PasteFormat CreateCustomAIPasteFormat(string name, string prompt, bool isSavedQuery, string providerId = null) =>
+ PasteFormat.CreateCustomAIFormat(CustomAIFormat, name, prompt, isSavedQuery, AvailableClipboardFormats, IsCustomAIServiceEnabled, providerId);
+
+ private string GetProviderIdForFormat(PasteFormats format) =>
+ format switch
+ {
+ PasteFormats.FixSpellingAndGrammar => _userSettings.FixSpellingAndGrammarProviderId,
+ _ => string.Empty,
+ };
private void UpdateAIProviderActiveFlags()
{
@@ -418,7 +430,7 @@ void UpdateFormats(ObservableCollection collection, IEnumerable CreateCustomAIPasteFormat(customAction.Name, customAction.Prompt, isSavedQuery: true)) : []);
+ IsCustomAIServiceEnabled ? _userSettings.CustomActions.Select(customAction => CreateCustomAIPasteFormat(customAction.Name, customAction.Prompt, isSavedQuery: true, customAction.ProviderId)) : []);
}
public void Dispose()
@@ -539,6 +551,7 @@ public async Task OnShowAsync()
{
PasteActionError = PasteActionError.None;
Query = string.Empty;
+ CoachingExplanation = null;
await ReadClipboardAsync();
@@ -616,6 +629,12 @@ public string CustomAIUnavailableErrorText
[ObservableProperty]
private string _customFormatResult;
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(HasCoachingExplanation))]
+ private string _coachingExplanation;
+
+ public bool HasCoachingExplanation => !string.IsNullOrEmpty(CoachingExplanation);
+
[RelayCommand]
public async Task PasteCustomAsync()
{
@@ -661,17 +680,37 @@ public void NextCustomFormat()
[RelayCommand]
public void OpenSettings()
{
- SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.AdvancedPaste);
+ try
+ {
+ var exePath = System.IO.Path.Combine(
+ ManagedCommon.PowerToysPathResolver.GetPowerToysInstallPath(),
+ "PowerToys.exe");
+
+ if (exePath != null && System.IO.File.Exists(exePath))
+ {
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = exePath,
+ Arguments = "--open-settings=AdvancedPaste",
+ UseShellExecute = false,
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to open settings", ex);
+ }
+
GetMainWindow()?.Close();
}
- internal async Task ExecutePasteFormatAsync(PasteFormats format, PasteActionSource source)
+ internal async Task ExecutePasteFormatAsync(PasteFormats format, PasteActionSource source, bool forceCoaching = false)
{
await ReadClipboardAsync();
- await ExecutePasteFormatAsync(CreateStandardPasteFormat(format), source);
+ await ExecutePasteFormatAsync(CreateStandardPasteFormat(format), source, forceCoaching);
}
- internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
+ internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, bool forceCoaching = false)
{
if (IsBusy)
{
@@ -704,12 +743,30 @@ internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteAction
await delayTask;
var outputText = await dataPackage.GetView().GetTextOrEmptyAsync();
+ bool isCoachingAction = pasteFormat.Format == PasteFormats.FixSpellingAndGrammar &&
+ (forceCoaching || (_userSettings.FixSpellingAndGrammarCoachingEnabled && !_userSettings.FixSpellingAndGrammarCoachingShortcutSet));
bool shouldPreview = pasteFormat.Metadata.CanPreview && _userSettings.ShowCustomPreview && !string.IsNullOrEmpty(outputText) && source != PasteActionSource.GlobalKeyboardShortcut;
+ // Coaching mode forces preview even for global keyboard shortcuts
+ if (isCoachingAction && !string.IsNullOrEmpty(outputText))
+ {
+ shouldPreview = true;
+ }
+
if (shouldPreview)
{
GeneratedResponses.Add(outputText);
CurrentResponseIndex = GeneratedResponses.Count - 1;
+
+ if (isCoachingAction)
+ {
+ await GenerateCoachingExplanationAsync(outputText);
+ }
+ else
+ {
+ CoachingExplanation = null;
+ }
+
PreviewRequested?.Invoke(this, EventArgs.Empty);
}
else
@@ -730,6 +787,65 @@ internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteAction
Logger.LogDebug($"Finished executing {pasteFormat.Format} from source {source}; timeTakenMs={elapsedWatch.ElapsedMilliseconds}");
}
+ private async Task GenerateCoachingExplanationAsync(string correctedText)
+ {
+ try
+ {
+ var originalText = ClipboardData != null ? await ClipboardData.GetTextOrEmptyAsync() : string.Empty;
+
+ if (string.IsNullOrEmpty(originalText))
+ {
+ CoachingExplanation = null;
+ return;
+ }
+
+ static string NormalizeForComparison(string s) =>
+ s.Replace('\u2018', '\'') // left single quote
+ .Replace('\u2019', '\'') // right single quote / apostrophe
+ .Replace('\u201C', '"') // left double quote
+ .Replace('\u201D', '"') // right double quote
+ .Replace('\u2013', '-') // en dash
+ .Replace('\u2014', '-'); // em dash
+
+ if (string.Equals(NormalizeForComparison(originalText), NormalizeForComparison(correctedText), StringComparison.Ordinal))
+ {
+ CoachingExplanation = null;
+ return;
+ }
+
+ var coachingInstruction = string.IsNullOrWhiteSpace(_userSettings.FixSpellingAndGrammarCoachingPrompt)
+ ? AdvancedPasteDefaultPrompts.FixSpellingAndGrammarCoaching
+ : _userSettings.FixSpellingAndGrammarCoachingPrompt;
+ var coachingInputText = $"Original:\n\"{originalText}\"\n\nCorrected:\n\"{correctedText}\"";
+
+ var coachingSystemPrompt = string.IsNullOrWhiteSpace(_userSettings.FixSpellingAndGrammarCoachingSystemPrompt)
+ ? AdvancedPasteDefaultPrompts.FixSpellingAndGrammarCoachingSystem
+ : _userSettings.FixSpellingAndGrammarCoachingSystemPrompt;
+
+ var coachingProviderId = _userSettings.FixSpellingAndGrammarCoachingProviderId;
+ if (string.IsNullOrWhiteSpace(coachingProviderId))
+ {
+ coachingProviderId = _userSettings.FixSpellingAndGrammarProviderId;
+ }
+
+ var result = await _customActionTransformService.TransformAsync(
+ coachingInstruction,
+ coachingInputText,
+ null,
+ _pasteActionCancellationTokenSource?.Token ?? CancellationToken.None,
+ null,
+ coachingSystemPrompt,
+ string.IsNullOrWhiteSpace(coachingProviderId) ? null : coachingProviderId);
+
+ CoachingExplanation = result?.Content;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Error generating coaching explanation", ex);
+ CoachingExplanation = null;
+ }
+ }
+
internal async Task ExecutePasteFormatAsync(VirtualKey key)
{
var pasteFormat = StandardPasteFormats.Concat(CustomActionPasteFormats)
diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPaste.base.rc b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPaste.base.rc
index b30e3923c989..eb9e4e22d1c0 100644
--- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPaste.base.rc
+++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPaste.base.rc
@@ -1,40 +1,6 @@
#include
#include "resource.h"
-#include "../../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
#include "winres.h"
#undef APSTUDIO_READONLY_SYMBOLS
-
-1 VERSIONINFO
-FILEVERSION FILE_VERSION
-PRODUCTVERSION PRODUCT_VERSION
-FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
-#ifdef _DEBUG
-FILEFLAGS VS_FF_DEBUG
-#else
-FILEFLAGS 0x0L
-#endif
-FILEOS VOS_NT_WINDOWS32
-FILETYPE VFT_DLL
-FILESUBTYPE VFT2_UNKNOWN
-BEGIN
- BLOCK "StringFileInfo"
- BEGIN
- BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
- BEGIN
- VALUE "CompanyName", COMPANY_NAME
- VALUE "FileDescription", FILE_DESCRIPTION
- VALUE "FileVersion", FILE_VERSION_STRING
- VALUE "InternalName", INTERNAL_NAME
- VALUE "LegalCopyright", COPYRIGHT_NOTE
- VALUE "OriginalFilename", ORIGINAL_FILENAME
- VALUE "ProductName", PRODUCT_NAME
- VALUE "ProductVersion", PRODUCT_VERSION_STRING
- END
- END
- BLOCK "VarFileInfo"
- BEGIN
- VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
- END
-END
diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj
index 9f7675799ea4..ca40cf792f0a 100644
--- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj
+++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj
@@ -15,7 +15,6 @@
DynamicLibrary
-
diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteProcessManager.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteProcessManager.cpp
index b202f93f4e10..aec3792beb51 100644
--- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteProcessManager.cpp
+++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteProcessManager.cpp
@@ -3,8 +3,11 @@
#include
#include
+#include
+#include
#include
#include
+#include
namespace
{
@@ -100,23 +103,110 @@ HRESULT AdvancedPasteProcessManager::start_process(const std::wstring& pipe_name
{
const unsigned long powertoys_pid = GetCurrentProcessId();
- const auto executable_args = std::format(L"{} {}", std::to_wstring(powertoys_pid), pipe_name);
+ // Ensure the AdvancedPaste MSIX package is registered
+ static const std::wstring packageName =
+#ifdef _DEBUG
+ L"PowerToys.AdvancedPaste.Dev";
+#else
+ L"PowerToys.AdvancedPaste";
+#endif
+
+ if (!package::GetRegisteredPackage(packageName, false).has_value())
+ {
+ Logger::info(L"AdvancedPaste MSIX not registered. Registering...");
+
+ const auto installationFolder = get_module_folderpath();
+
+#ifdef _DEBUG
+ auto msix = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\AdvancedPaste\\AppPackages\\PowerToys.AdvancedPaste_0.0.1.0_Debug_Test\\", false);
+ auto dependencies = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\AdvancedPaste\\AppPackages\\PowerToys.AdvancedPaste_0.0.1.0_Debug_Test\\Dependencies\\", true);
+#else
+ auto msix = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\AdvancedPaste\\", false);
+ auto dependencies = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\AdvancedPaste\\Dependencies\\", true);
+#endif
+
+ if (!msix.empty())
+ {
+ if (!package::RegisterPackage(msix[0], dependencies))
+ {
+ Logger::error(L"Failed to register AdvancedPaste MSIX package");
+ }
+ }
+ else
+ {
+ Logger::warn(L"No MSIX found for AdvancedPaste, falling back to direct exe launch");
+
+ // Fallback: launch exe directly (dev builds without GenerateAppxPackageOnBuild)
+ const auto executable_args = std::format(L"{} {}", std::to_wstring(powertoys_pid), pipe_name);
+
+ SHELLEXECUTEINFOW sei{ sizeof(sei) };
+ sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
+ sei.lpFile = L"WinUI3Apps\\AdvancedPaste\\PowerToys.AdvancedPaste.exe";
+ sei.nShow = SW_SHOWNORMAL;
+ sei.lpParameters = executable_args.data();
+ if (ShellExecuteExW(&sei))
+ {
+ Logger::trace("Successfully started Advanced Paste process (direct)");
+ terminate_process();
+ m_hProcess = sei.hProcess;
+ return S_OK;
+ }
+ else
+ {
+ Logger::error(L"Advanced Paste process failed to start. {}", get_last_error_or_default(GetLastError()));
+ return E_FAIL;
+ }
+ }
+ }
+
+ // Launch via protocol URI, passing PID and pipe name as query parameters
+ const auto protocolUri = std::format(L"x-advancedpaste://launch?pid={}&pipe={}", std::to_wstring(powertoys_pid), pipe_name);
SHELLEXECUTEINFOW sei{ sizeof(sei) };
- sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
- sei.lpFile = L"WinUI3Apps\\PowerToys.AdvancedPaste.exe";
+ sei.fMask = { SEE_MASK_FLAG_NO_UI };
+ sei.lpFile = protocolUri.c_str();
sei.nShow = SW_SHOWNORMAL;
- sei.lpParameters = executable_args.data();
if (ShellExecuteExW(&sei))
{
- Logger::trace("Successfully started Advanced Paste process");
- terminate_process();
- m_hProcess = sei.hProcess;
+ Logger::trace("Successfully started Advanced Paste via protocol");
+
+ // Find the AP process by name to track its lifetime
+ // Give it a moment to start up
+ for (int retry = 0; retry < 10; retry++)
+ {
+ Sleep(500);
+ HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+ if (snapshot != INVALID_HANDLE_VALUE)
+ {
+ PROCESSENTRY32W pe{ sizeof(pe) };
+ if (Process32FirstW(snapshot, &pe))
+ {
+ do
+ {
+ if (std::wstring(pe.szExeFile) == L"PowerToys.AdvancedPaste.exe")
+ {
+ auto hProc = OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE | PROCESS_TERMINATE, FALSE, pe.th32ProcessID);
+ if (hProc)
+ {
+ terminate_process();
+ m_hProcess = hProc;
+ Logger::trace(L"Found Advanced Paste process: PID {}", pe.th32ProcessID);
+ CloseHandle(snapshot);
+ return S_OK;
+ }
+ }
+ } while (Process32NextW(snapshot, &pe));
+ }
+ CloseHandle(snapshot);
+ }
+ }
+
+ Logger::warn("Advanced Paste process not found after protocol launch");
return S_OK;
}
else
{
- Logger::error(L"Advanced Paste process failed to start. {}", get_last_error_or_default(GetLastError()));
+ Logger::error(L"Advanced Paste protocol launch failed. {}", get_last_error_or_default(GetLastError()));
return E_FAIL;
}
}
@@ -175,8 +265,8 @@ HRESULT AdvancedPasteProcessManager::start_named_pipe_server(const std::wstring&
}
}
- // Wait for client.
- const constexpr DWORD client_timeout_millis = 5000;
+ // Wait for client. Protocol activation takes longer than direct exe launch.
+ const constexpr DWORD client_timeout_millis = 15000;
switch (WaitForSingleObject(overlapped.hEvent, client_timeout_millis))
{
case WAIT_OBJECT_0:
diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
index 17205687a54e..59e4e10ce549 100644
--- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
+++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
@@ -66,6 +66,8 @@ namespace
const wchar_t JSON_KEY_PROVIDERS[] = L"providers";
const wchar_t JSON_KEY_SERVICE_TYPE[] = L"service-type";
const wchar_t JSON_KEY_ENABLE_ADVANCED_AI[] = L"enable-advanced-ai";
+ const wchar_t JSON_KEY_COACHING_SHORTCUT[] = L"coaching-shortcut";
+ const wchar_t JSON_KEY_COACHING_ENABLED[] = L"coaching-enabled";
const wchar_t JSON_KEY_VALUE[] = L"value";
}
@@ -255,6 +257,21 @@ class AdvancedPaste : public PowertoyModuleIface
};
m_additional_actions.push_back(additionalAction);
+
+ // Register coaching shortcut as a separate hotkey with a "-coaching" suffix ID
+ if (action.HasKey(JSON_KEY_COACHING_SHORTCUT) && action.GetNamedBoolean(JSON_KEY_COACHING_ENABLED, false))
+ {
+ auto coachingHotkey = parse_single_hotkey(action.GetNamedObject(JSON_KEY_COACHING_SHORTCUT), actionIsShown);
+ if (coachingHotkey.key != 0)
+ {
+ const AdditionalAction coachingAction
+ {
+ std::wstring(actionName.c_str()) + L"-coaching",
+ coachingHotkey
+ };
+ m_additional_actions.push_back(coachingAction);
+ }
+ }
}
else
{
@@ -407,6 +424,7 @@ class AdvancedPaste : public PowertoyModuleIface
// Define the expected order to ensure consistent hotkey ID assignment
const std::vector expectedOrder = {
L"image-to-text",
+ L"fix-spelling-and-grammar",
L"paste-as-file",
L"transcode"
};
@@ -923,6 +941,12 @@ class AdvancedPaste : public PowertoyModuleIface
m_triggerEventWaiter.start(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_EVENT, [this](DWORD) {
// Same logic as hotkeyId == 1 (m_advanced_paste_ui_hotkey)
Logger::trace(L"AdvancedPaste ShowUI event triggered");
+
+ if (m_auto_copy_selection_custom_action)
+ {
+ send_copy_selection(); // best-effort; ignore failure
+ }
+
m_process_manager.start();
m_process_manager.bring_to_front();
m_process_manager.send_message(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_MESSAGE);
@@ -973,12 +997,11 @@ class AdvancedPaste : public PowertoyModuleIface
}
}
- if (is_custom_action_hotkey && m_auto_copy_selection_custom_action)
+ // Try to capture selected text for all hotkey actions when the setting is enabled.
+ // If nothing is selected (clipboard unchanged), fall through to use existing clipboard content.
+ if (m_auto_copy_selection_custom_action)
{
- if (!send_copy_selection())
- {
- return false;
- }
+ send_copy_selection(); // best-effort; ignore failure
}
m_process_manager.start();
diff --git a/src/modules/AdvancedPaste/custom.props b/src/modules/AdvancedPaste/custom.props
new file mode 100644
index 000000000000..3b4b52e42a8d
--- /dev/null
+++ b/src/modules/AdvancedPaste/custom.props
@@ -0,0 +1,11 @@
+
+
+
+
+ true
+ 2025
+ 0
+ 9
+ PowerToys Advanced Paste
+
+
diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props
index 435f8b977a7f..5c6ab89881a8 100644
--- a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props
+++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props
@@ -10,7 +10,7 @@
-
+
diff --git a/src/settings-ui/Settings.UI.Library/AIServiceType.cs b/src/settings-ui/Settings.UI.Library/AIServiceType.cs
index 27eccff1cf6e..e30ebbb5d2a7 100644
--- a/src/settings-ui/Settings.UI.Library/AIServiceType.cs
+++ b/src/settings-ui/Settings.UI.Library/AIServiceType.cs
@@ -19,5 +19,6 @@ public enum AIServiceType
Google,
AzureAIInference,
Ollama,
+ PhiSilica,
}
}
diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs
index 5b19212ebaf2..887c0dc78e50 100644
--- a/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs
+++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs
@@ -31,6 +31,7 @@ public static AIServiceType ToAIServiceType(this string serviceType)
"google" or "googleai" or "googlegemini" => AIServiceType.Google,
"azureaiinference" or "azureinference" => AIServiceType.AzureAIInference,
"ollama" => AIServiceType.Ollama,
+ "phisilica" or "phi" or "philm" => AIServiceType.PhiSilica,
_ => AIServiceType.Unknown,
};
}
@@ -51,6 +52,7 @@ public static string ToConfigurationString(this AIServiceType serviceType)
AIServiceType.Google => "Google",
AIServiceType.AzureAIInference => "AzureAIInference",
AIServiceType.Ollama => "Ollama",
+ AIServiceType.PhiSilica => "PhiSilica",
AIServiceType.Unknown => string.Empty,
_ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, "Unsupported AI service type."),
};
@@ -72,6 +74,7 @@ public static string ToNormalizedKey(this AIServiceType serviceType)
AIServiceType.Google => "google",
AIServiceType.AzureAIInference => "azureaiinference",
AIServiceType.Ollama => "ollama",
+ AIServiceType.PhiSilica => "phisilica",
_ => string.Empty,
};
}
diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs
index 653b85553e39..2c45d9bae60a 100644
--- a/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs
+++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs
@@ -118,6 +118,15 @@ public static class AIServiceTypeRegistry
PrivacyLabel = "AdvancedPaste_OpenAI_PrivacyLabel",
PrivacyUri = new Uri("https://openai.com/privacy"),
},
+ [AIServiceType.PhiSilica] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.PhiSilica,
+ DisplayName = "Phi Silica",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/WindowsML.svg",
+ IsOnlineService = false,
+ IsLocalModel = true,
+ LegalDescription = "AdvancedPaste_LocalModel_LegalDescription",
+ },
[AIServiceType.Unknown] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.Unknown,
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs
index 1642ecf9c421..6e46e54e4b64 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs
@@ -12,7 +12,15 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvancedPasteAction
{
private HotkeySettings _shortcut = new();
+ private HotkeySettings _coachingShortcut = new();
private bool _isShown;
+ private string _prompt = string.Empty;
+ private string _systemPrompt = string.Empty;
+ private string _coachingPrompt = string.Empty;
+ private string _coachingSystemPrompt = string.Empty;
+ private string _providerId = string.Empty;
+ private string _coachingProviderId = string.Empty;
+ private bool _coachingEnabled;
private bool _hasConflict;
private string _tooltip;
@@ -33,6 +41,20 @@ public HotkeySettings Shortcut
}
}
+ [JsonPropertyName("coaching-shortcut")]
+ public HotkeySettings CoachingShortcut
+ {
+ get => _coachingShortcut;
+ set
+ {
+ if (_coachingShortcut != value)
+ {
+ _coachingShortcut = value ?? new();
+ OnPropertyChanged();
+ }
+ }
+ }
+
[JsonPropertyName("isShown")]
public bool IsShown
{
@@ -40,6 +62,55 @@ public bool IsShown
set => Set(ref _isShown, value);
}
+ [JsonPropertyName("prompt")]
+ public string Prompt
+ {
+ get => _prompt;
+ set => Set(ref _prompt, value ?? string.Empty);
+ }
+
+ [JsonPropertyName("system-prompt")]
+ public string SystemPrompt
+ {
+ get => _systemPrompt;
+ set => Set(ref _systemPrompt, value ?? string.Empty);
+ }
+
+ [JsonPropertyName("coaching-prompt")]
+ public string CoachingPrompt
+ {
+ get => _coachingPrompt;
+ set => Set(ref _coachingPrompt, value ?? string.Empty);
+ }
+
+ [JsonPropertyName("coaching-system-prompt")]
+ public string CoachingSystemPrompt
+ {
+ get => _coachingSystemPrompt;
+ set => Set(ref _coachingSystemPrompt, value ?? string.Empty);
+ }
+
+ [JsonPropertyName("provider-id")]
+ public string ProviderId
+ {
+ get => _providerId;
+ set => Set(ref _providerId, value ?? string.Empty);
+ }
+
+ [JsonPropertyName("coaching-provider-id")]
+ public string CoachingProviderId
+ {
+ get => _coachingProviderId;
+ set => Set(ref _coachingProviderId, value ?? string.Empty);
+ }
+
+ [JsonPropertyName("coaching-enabled")]
+ public bool CoachingEnabled
+ {
+ get => _coachingEnabled;
+ set => Set(ref _coachingEnabled, value);
+ }
+
[JsonIgnore]
public bool HasConflict
{
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs
index b193c01c74ee..b476f50f674a 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs
@@ -11,12 +11,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed class AdvancedPasteAdditionalActions
{
private AdvancedPasteAdditionalAction _imageToText = new();
+ private AdvancedPasteAdditionalAction _fixSpellingAndGrammar = new();
private AdvancedPastePasteAsFileAction _pasteAsFile = new();
private AdvancedPasteTranscodeAction _transcode = new();
public static class PropertyNames
{
public const string ImageToText = "image-to-text";
+ public const string FixSpellingAndGrammar = "fix-spelling-and-grammar";
public const string PasteAsFile = "paste-as-file";
public const string Transcode = "transcode";
}
@@ -28,6 +30,13 @@ public AdvancedPasteAdditionalAction ImageToText
init => _imageToText = value ?? new();
}
+ [JsonPropertyName(PropertyNames.FixSpellingAndGrammar)]
+ public AdvancedPasteAdditionalAction FixSpellingAndGrammar
+ {
+ get => _fixSpellingAndGrammar;
+ init => _fixSpellingAndGrammar = value ?? new();
+ }
+
[JsonPropertyName(PropertyNames.PasteAsFile)]
public AdvancedPastePasteAsFileAction PasteAsFile
{
@@ -44,7 +53,7 @@ public AdvancedPasteTranscodeAction Transcode
public IEnumerable GetAllActions()
{
- return GetAllActionsRecursive([ImageToText, PasteAsFile, Transcode]);
+ return GetAllActionsRecursive([ImageToText, FixSpellingAndGrammar, PasteAsFile, Transcode]);
}
///
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs
index c98129590698..4a3a763c4c94 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs
@@ -16,6 +16,7 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private string _name = string.Empty;
private string _description = string.Empty;
private string _prompt = string.Empty;
+ private string _providerId = string.Empty;
private HotkeySettings _shortcut = new();
private bool _isShown;
private bool _canMoveUp;
@@ -64,6 +65,13 @@ public string Prompt
}
}
+ [JsonPropertyName("provider-id")]
+ public string ProviderId
+ {
+ get => _providerId;
+ set => Set(ref _providerId, value ?? string.Empty);
+ }
+
[JsonPropertyName("shortcut")]
public HotkeySettings Shortcut
{
@@ -138,6 +146,7 @@ public void Update(AdvancedPasteCustomAction other)
Name = other.Name;
Description = other.Description;
Prompt = other.Prompt;
+ ProviderId = other.ProviderId;
Shortcut = other.GetShortcutClone();
IsShown = other.IsShown;
CanMoveUp = other.CanMoveUp;
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteDefaultPrompts.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteDefaultPrompts.cs
new file mode 100644
index 000000000000..c58170f7320f
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteDefaultPrompts.cs
@@ -0,0 +1,20 @@
+// 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.PowerToys.Settings.UI.Library;
+
+///
+/// Shared default prompts for built-in AI actions. Referenced by both the AdvancedPaste module
+/// and the Settings UI to ensure consistent defaults and enable "reset to default" functionality.
+///
+public static class AdvancedPasteDefaultPrompts
+{
+ public const string FixSpellingAndGrammar = "Fix all spelling and grammar errors in the following text. Return only the corrected text without any additional explanation or commentary.";
+
+ public const string FixSpellingAndGrammarSystem = "You are a professional proofreader. You fix spelling and grammar errors in text. You return only the corrected text with no commentary.";
+
+ public const string FixSpellingAndGrammarCoaching = "Briefly explain what was changed and why in terms of language rules. Be concise as reviewer.";
+
+ public const string FixSpellingAndGrammarCoachingSystem = "You are a writing coach and language teacher. You will be given an original sentence and a corrected version.";
+}
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs
index be001fd9d6a1..e03e9c6262df 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs
@@ -96,6 +96,13 @@ public HotkeyAccessor[] GetAllHotkeyAccessors()
customAction.Name));
}
+ // Coaching shortcut for Fix Spelling and Grammar
+ var fixSpellingAction = Properties.AdditionalActions.FixSpellingAndGrammar;
+ hotkeyAccessors.Add(new HotkeyAccessor(
+ () => fixSpellingAction.CoachingShortcut,
+ value => fixSpellingAction.CoachingShortcut = value ?? new HotkeySettings(),
+ "FixSpellingAndGrammarCoaching"));
+
return hotkeyAccessors.ToArray();
}
diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
index e726063b819e..bae384d99531 100644
--- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
+++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
@@ -19,6 +19,7 @@
PowerToys.Settings.pri
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
index 5d1fa671ac7b..d5becba90a05 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
@@ -116,6 +116,17 @@
Header="{x:Bind ModelName, Mode=OneWay}"
HeaderIcon="{x:Bind ServiceType, Mode=OneWay, Converter={StaticResource ServiceTypeToIconConverter}}">
+
+
+
+
-
+
@@ -195,7 +211,7 @@
Name="PasteAsPlainTextShortcut"
x:Uid="PasteAsPlainText_Shortcut"
HeaderIcon="{ui:FontIcon Glyph=}">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -542,6 +683,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -83,6 +87,7 @@ public void RefreshEnabledState()
ViewModel.RefreshEnabledState();
UpdatePasteAIUIVisibility();
_ = UpdateFoundryLocalUIAsync();
+ _ = UpdatePhiSilicaUIAsync();
}
private void EnableAdvancedPasteAI() => ViewModel.EnableAI();
@@ -103,6 +108,8 @@ private void AdvancedPaste_EnableAIToggle_Toggled(object sender, RoutedEventArgs
else
{
ViewModel.DisableAI();
+ FixSpellingAndGrammar.IsExpanded = false;
+ AdvancedPasteUIActions.IsExpanded = false;
}
}
@@ -319,7 +326,9 @@ private void UpdatePasteAIUIVisibility()
bool requiresApiVersion = serviceKind == AIServiceType.AzureOpenAI;
bool requiresModelPath = serviceKind == AIServiceType.Onnx;
bool isFoundryLocal = serviceKind == AIServiceType.FoundryLocal;
+ bool isPhiSilica = serviceKind == AIServiceType.PhiSilica;
bool requiresApiKey = RequiresApiKeyForService(selectedType);
+ bool requiresModelName = !isFoundryLocal && !isPhiSilica;
bool showModerationToggle = serviceKind == AIServiceType.OpenAI;
bool showAdvancedAI = serviceKind == AIServiceType.OpenAI || serviceKind == AIServiceType.AzureOpenAI;
@@ -344,7 +353,7 @@ private void UpdatePasteAIUIVisibility()
PasteAIModerationToggle.Visibility = showModerationToggle ? Visibility.Visible : Visibility.Collapsed;
PasteAIEnableAdvancedAICheckBox.Visibility = showAdvancedAI ? Visibility.Visible : Visibility.Collapsed;
PasteAIApiKeyPasswordBox.Visibility = requiresApiKey ? Visibility.Visible : Visibility.Collapsed;
- PasteAIModelNameTextBox.Visibility = isFoundryLocal ? Visibility.Collapsed : Visibility.Visible;
+ PasteAIModelNameTextBox.Visibility = requiresModelName ? Visibility.Visible : Visibility.Collapsed;
if (requiresApiKey)
{
@@ -373,6 +382,11 @@ private void UpdatePasteAIUIVisibility()
// For Foundry Local, UpdateFoundrySaveButtonState will handle button state
// based on model selection status
}
+ else if (isPhiSilica)
+ {
+ // For Phi Silica, UpdatePhiSilicaUIAsync will handle button state
+ // based on device availability
+ }
else
{
// GPO allows this provider, enable save button
@@ -421,6 +435,177 @@ private Task UpdateFoundryLocalUIAsync()
return Task.CompletedTask;
}
+ private async Task UpdatePhiSilicaUIAsync()
+ {
+ string selectedType = ViewModel?.PasteAIProviderDraft?.ServiceType ?? string.Empty;
+ bool isPhiSilica = string.Equals(selectedType, "PhiSilica", StringComparison.OrdinalIgnoreCase);
+
+ if (PhiSilicaPanel is not null)
+ {
+ PhiSilicaPanel.Visibility = isPhiSilica ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ if (!isPhiSilica)
+ {
+ _isPhiSilicaAvailable = false;
+ return;
+ }
+
+ if (PasteAIProviderConfigurationDialog is not null)
+ {
+ PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
+ }
+
+ ShowPhiSilicaLoadingState();
+
+ try
+ {
+ // Settings doesn't have package identity, so it can't call
+ // LanguageModel.GetReadyState() directly. Instead, probe via AdvancedPaste
+ // which runs with its own package identity. See microsoft-ui-xaml#10856.
+ var result = await Task.Run(() => CheckPhiSilicaViaAdvancedPaste());
+
+ if (result == "NotSupported")
+ {
+ _isPhiSilicaAvailable = false;
+ ShowPhiSilicaNotAvailableState(
+ "Phi Silica is not available on this device.",
+ "A Copilot+ PC with an NPU is required to use Phi Silica. For on-device AI on any Windows PC, consider using Foundry Local.");
+ }
+ else if (result == "NotReady")
+ {
+ _isPhiSilicaAvailable = false;
+ ShowPhiSilicaNotAvailableState(
+ "Phi Silica model is not ready.",
+ "The model needs to be downloaded. Check Windows Update for the AI model download progress.");
+ }
+ else
+ {
+ _isPhiSilicaAvailable = true;
+ ShowPhiSilicaAvailableState("Phi Silica is available and ready on this device.");
+ }
+ }
+ catch (Exception)
+ {
+ _isPhiSilicaAvailable = false;
+ ShowPhiSilicaNotAvailableState(
+ "Phi Silica is not available on this device.",
+ "Unable to check Phi Silica availability. A Copilot+ PC with an NPU is required.");
+ }
+
+ if (PasteAIProviderConfigurationDialog is not null)
+ {
+ PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = _isPhiSilicaAvailable;
+ }
+ }
+
+ private void ShowPhiSilicaLoadingState()
+ {
+ if (PhiSilicaLoadingPanel is not null)
+ {
+ PhiSilicaLoadingPanel.Visibility = Visibility.Visible;
+ }
+
+ if (PhiSilicaAvailablePanel is not null)
+ {
+ PhiSilicaAvailablePanel.Visibility = Visibility.Collapsed;
+ }
+
+ if (PhiSilicaNotAvailablePanel is not null)
+ {
+ PhiSilicaNotAvailablePanel.Visibility = Visibility.Collapsed;
+ }
+ }
+
+ private void ShowPhiSilicaAvailableState(string message)
+ {
+ if (PhiSilicaLoadingPanel is not null)
+ {
+ PhiSilicaLoadingPanel.Visibility = Visibility.Collapsed;
+ }
+
+ if (PhiSilicaAvailablePanel is not null)
+ {
+ PhiSilicaAvailablePanel.Visibility = Visibility.Visible;
+ }
+
+ if (PhiSilicaAvailableText is not null)
+ {
+ PhiSilicaAvailableText.Text = message;
+ }
+
+ if (PhiSilicaNotAvailablePanel is not null)
+ {
+ PhiSilicaNotAvailablePanel.Visibility = Visibility.Collapsed;
+ }
+ }
+
+ private void ShowPhiSilicaNotAvailableState(string title, string description)
+ {
+ if (PhiSilicaLoadingPanel is not null)
+ {
+ PhiSilicaLoadingPanel.Visibility = Visibility.Collapsed;
+ }
+
+ if (PhiSilicaAvailablePanel is not null)
+ {
+ PhiSilicaAvailablePanel.Visibility = Visibility.Collapsed;
+ }
+
+ if (PhiSilicaNotAvailablePanel is not null)
+ {
+ PhiSilicaNotAvailablePanel.Visibility = Visibility.Visible;
+ }
+
+ if (PhiSilicaNotAvailableTitle is not null)
+ {
+ PhiSilicaNotAvailableTitle.Text = title;
+ }
+
+ if (PhiSilicaNotAvailableDescription is not null)
+ {
+ PhiSilicaNotAvailableDescription.Text = description;
+ }
+ }
+
+ ///
+ /// Checks Phi Silica availability by querying the running AdvancedPaste process
+ /// via a well-known named pipe. AdvancedPaste runs as a packaged MSIX with identity
+ /// and checks the API status on startup.
+ /// Returns "Available", "NotReady", or "NotSupported".
+ ///
+ private static string CheckPhiSilicaViaAdvancedPaste()
+ {
+ try
+ {
+ using var client = new System.IO.Pipes.NamedPipeClientStream(
+ ".",
+ "powertoys_advancedpaste_phi_status",
+ System.IO.Pipes.PipeDirection.In);
+
+ client.Connect(timeout: 5000);
+
+ using var reader = new StreamReader(client, System.Text.Encoding.UTF8);
+ var status = reader.ReadToEnd().Trim();
+
+ return status switch
+ {
+ "Available" => "Available",
+ "NotReady" => "NotReady",
+ _ => "NotSupported",
+ };
+ }
+ catch (TimeoutException)
+ {
+ // AdvancedPaste not running or pipe not ready
+ return "NotSupported";
+ }
+ catch
+ {
+ return "NotSupported";
+ }
+ }
+
private async Task LoadFoundryLocalModelsAsync()
{
if (FoundryLocalPanel is null)
@@ -835,6 +1020,7 @@ private static bool RequiresApiKeyForService(string serviceType)
AIServiceType.Onnx => false,
AIServiceType.Ollama => false,
AIServiceType.FoundryLocal => false,
+ AIServiceType.PhiSilica => false,
AIServiceType.ML => false,
_ => true,
};
@@ -1112,6 +1298,7 @@ private async void ProviderMenuFlyoutItem_Click(object sender, RoutedEventArgs e
}
await UpdateFoundryLocalUIAsync();
+ await UpdatePhiSilicaUIAsync();
UpdatePasteAIUIVisibility();
RefreshDialogBindings();
@@ -1141,6 +1328,7 @@ private async void EditPasteAIProviderButton_Click(object sender, RoutedEventArg
UpdatePasteAIUIVisibility();
await UpdateFoundryLocalUIAsync();
+ await UpdatePhiSilicaUIAsync();
RefreshDialogBindings();
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(provider.Id, provider.ServiceType);
await PasteAIProviderConfigurationDialog.ShowAsync();
@@ -1157,11 +1345,41 @@ private void RemovePasteAIProviderButton_Click(object sender, RoutedEventArgs e)
ViewModel?.RemovePasteAIProvider(provider);
}
+ private void SetAsDefaultProviderButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not PasteAIProviderDefinition provider)
+ {
+ return;
+ }
+
+ ViewModel?.SetAsDefaultProvider(provider);
+ }
+
private void PasteAIProviderConfigurationDialog_Closed(ContentDialog sender, ContentDialogClosedEventArgs args)
{
ViewModel?.CancelPasteAIProviderDraft();
PasteAIProviderConfigurationDialog.Title = PasteAiDialogDefaultTitle;
PasteAIApiKeyPasswordBox.Password = string.Empty;
}
+
+ private void ClearProviderSelection_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button button && button.Tag is ComboBox comboBox)
+ {
+ comboBox.SelectedIndex = -1;
+ }
+ }
+
+ private string GetDefaultProviderLabel()
+ {
+ try
+ {
+ return Microsoft.PowerToys.Settings.UI.Helpers.ResourceLoaderInstance.ResourceLoader.GetString("AdvancedPaste_ActionProvider_Default");
+ }
+ catch
+ {
+ return "Default (use active provider)";
+ }
+ }
}
}
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index cd3c6922912f..5a317f4326d4 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -1927,8 +1927,8 @@ Made with 💗 by Microsoft and the PowerToys community.
Open editor
-
-
+
+
Paste as plain text directly
@@ -1981,6 +1981,86 @@ Made with 💗 by Microsoft and the PowerToys community.
Image to text
+
+ Fix spelling and grammar
+
+
+ Custom prompt
+
+
+ Override the default AI prompt used for fixing spelling and grammar. Leave empty to use the default prompt.
+
+
+ System prompt
+ Header for the system prompt customization for fix spelling action
+
+
+ Override the system prompt that sets the AI's role for fixing spelling and grammar. Leave empty to use the default.
+ Description for the system prompt customization
+
+
+ AI model
+ Label for selecting which AI model provider to use for this action
+
+
+ Select the AI model to use for this action. Leave as default to use the globally active provider.
+
+
+ Default (use active provider)
+ Option in provider picker that means use the globally configured provider
+
+
+ AI model
+ Label for selecting the AI model provider in the custom action dialog
+
+
+ Reset to default provider
+ Tooltip for button that clears the provider selection back to default
+
+
+ Reset to default provider
+ Accessible name for button that clears the provider selection back to default
+
+
+ Coaching mode
+ Header for the coaching mode collapsible settings section
+
+
+ When enabled, shows a preview with an explanation of what was fixed and why.
+ Description for the coaching mode settings section
+
+
+ Coaching shortcut
+ Header for the coaching mode keyboard shortcut
+
+
+ Optional shortcut that triggers fix spelling with coaching enabled. If not set, uses the main action shortcut.
+ Description for the coaching shortcut setting
+
+
+ Coaching prompt
+ Header for the coaching prompt customization text box
+
+
+ Coaching AI model
+ Label for selecting which AI model provider to use for coaching explanations
+
+
+ Select the AI model to use for coaching explanations. Leave as default to use the same model as Fix Spelling.
+ Description for the coaching provider selector
+
+
+ Override the AI prompt used when generating the coaching explanation. Leave empty to use the default prompt.
+ Description for the coaching prompt customization
+
+
+ Coaching system prompt
+ Header for the coaching system prompt customization text box
+
+
+ Override the system prompt that sets the AI's role for coaching explanations. Leave empty to use the default.
+ Description for the coaching system prompt customization
+
Paste as file
@@ -2563,7 +2643,7 @@ From there, simply click on one of the supported files in the File Explorer and
Holding Shift
CursorWrap: Activation mode - wrap only when Shift held
-
+
Mouse Pointer Crosshairs
Mouse as in the hardware peripheral.
@@ -4379,11 +4459,11 @@ Activate by holding the key for the character you want to add an accent to, then
Enables display of clipboard contents preview in the Advanced Paste window
- Auto-copy selection for custom action hotkeys
+ Use selected text for all hotkeys
Advanced Paste is a product name
- Attempts to copy the current selection before running a custom action shortcut
+ Attempts to copy the current text selection before running any Advanced Paste shortcut. Falls back to clipboard contents if nothing is selected.
Advanced Paste is a product name
@@ -5754,6 +5834,14 @@ The break timer font matches the text font.
Edit
+
+ Set as default
+ Menu item to mark a provider as the default/active one
+
+
+ Default
+ Badge label shown on the provider that is currently the default/active one
+
Remove
diff --git a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
index ad75c72d105f..c155f0f092d1 100644
--- a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
@@ -492,6 +492,7 @@ public PasteAIConfiguration PasteAIConfiguration
var newValue = value ?? new PasteAIConfiguration();
_advancedPasteSettings.Properties.PasteAIConfiguration = newValue;
+ SyncProviderActiveFlags(newValue);
SubscribeToPasteAIConfiguration(newValue);
OnPropertyChanged(nameof(PasteAIConfiguration));
@@ -781,6 +782,25 @@ public void RemovePasteAIProvider(PasteAIProviderDefinition provider)
}
}
+ public void SetAsDefaultProvider(PasteAIProviderDefinition provider)
+ {
+ if (provider is null || string.IsNullOrEmpty(provider.Id))
+ {
+ return;
+ }
+
+ var config = PasteAIConfiguration;
+ if (config is null)
+ {
+ return;
+ }
+
+ config.ActiveProviderId = provider.Id;
+ SyncProviderActiveFlags(config);
+ SaveAndNotifySettings();
+ OnPropertyChanged(nameof(PasteAIConfiguration));
+ }
+
protected override void Dispose(bool disposing)
{
if (!_disposed)
@@ -1324,7 +1344,7 @@ private static bool ShouldReplacePasteAIConfiguration(PasteAIConfiguration curre
return true;
}
- if (existing?.ModerationEnabled != updated?.ModerationEnabled || existing?.EnableAdvancedAI != updated?.EnableAdvancedAI || existing?.IsActive != updated?.IsActive)
+ if (existing?.ModerationEnabled != updated?.ModerationEnabled || existing?.EnableAdvancedAI != updated?.EnableAdvancedAI)
{
return true;
}
@@ -1411,6 +1431,12 @@ private void OnPasteAIProviderPropertyChanged(object sender, PropertyChangedEven
{
if (sender is PasteAIProviderDefinition provider)
{
+ // IsActive is a UI-only (JsonIgnore) flag; don't save when it changes.
+ if (string.Equals(e.PropertyName, nameof(PasteAIProviderDefinition.IsActive), StringComparison.Ordinal))
+ {
+ return;
+ }
+
// When service type changes we may need to update credentials entry names.
if (string.Equals(e.PropertyName, nameof(PasteAIProviderDefinition.ServiceType), StringComparison.Ordinal))
{
@@ -1427,12 +1453,14 @@ private void OnPasteAIConfigurationPropertyChanged(object sender, PropertyChange
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.Providers), StringComparison.Ordinal))
{
SubscribeToPasteAIProviders(PasteAIConfiguration);
+ SyncProviderActiveFlags(PasteAIConfiguration);
SaveAndNotifySettings();
return;
}
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ActiveProviderId), StringComparison.Ordinal))
{
+ SyncProviderActiveFlags(PasteAIConfiguration);
SaveAndNotifySettings();
}
}
@@ -1448,9 +1476,32 @@ private void InitializePasteAIProviderState()
pasteConfig.Providers ??= new ObservableCollection();
+ SyncProviderActiveFlags(pasteConfig);
SubscribeToPasteAIProviders(pasteConfig);
}
+ private static void SyncProviderActiveFlags(PasteAIConfiguration config)
+ {
+ if (config?.Providers is null)
+ {
+ return;
+ }
+
+ var activeId = config.ActiveProviderId;
+
+ // If no explicit active ID, default to the first provider
+ if (string.IsNullOrEmpty(activeId) && config.Providers.Count > 0)
+ {
+ activeId = config.Providers[0].Id;
+ config.ActiveProviderId = activeId;
+ }
+
+ foreach (var provider in config.Providers)
+ {
+ provider.IsActive = !string.IsNullOrEmpty(activeId) && string.Equals(provider.Id, activeId, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
private static string RetrieveCredentialValue(string credentialResource, string credentialUserName)
{
if (string.IsNullOrWhiteSpace(credentialResource) || string.IsNullOrWhiteSpace(credentialUserName))
diff --git a/tools/Verification scripts/verify-installation-script.ps1 b/tools/Verification scripts/verify-installation-script.ps1
index d617aca9a71e..75f73ec8c5ab 100644
--- a/tools/Verification scripts/verify-installation-script.ps1
+++ b/tools/Verification scripts/verify-installation-script.ps1
@@ -468,8 +468,6 @@ function Test-CoreFiles {
$winUI3SignedFiles = @(
'PowerToys.Settings.dll',
'PowerToys.Settings.exe',
- 'PowerToys.AdvancedPaste.exe',
- 'PowerToys.AdvancedPaste.dll',
'PowerToys.HostsModuleInterface.dll',
'PowerToys.HostsUILib.dll',
'PowerToys.Hosts.dll',
@@ -724,6 +722,26 @@ function Test-CommandPalettePackages {
}
}
+function Test-AdvancedPastePackage {
+ param(
+ [string]$InstallPath
+ )
+
+ $advancedPastePath = Join-Path $InstallPath "WinUI3Apps\AdvancedPaste"
+ if (Test-Path $advancedPastePath) {
+ $msixFiles = Get-ChildItem $advancedPastePath -Filter "*.msix" -ErrorAction SilentlyContinue
+ if ($msixFiles) {
+ Add-CheckResult -Category "Advanced Paste" -CheckName "AdvancedPaste MSIX Package" -Status 'Pass' -Message "Found $($msixFiles.Count) Advanced Paste MSIX package(s)"
+ }
+ else {
+ Add-CheckResult -Category "Advanced Paste" -CheckName "AdvancedPaste MSIX Package" -Status 'Warning' -Message "No Advanced Paste MSIX packages found"
+ }
+ }
+ else {
+ Add-CheckResult -Category "Advanced Paste" -CheckName "AdvancedPaste Module" -Status 'Warning' -Message "Advanced Paste module not found at: $advancedPastePath"
+ }
+}
+
function Test-ContextMenuPackages {
param(
[string]$InstallPath