Compare commits

...

107 Commits

Author SHA1 Message Date
khmyznikov
780abe199c add more details for API state 2026-06-16 16:43:55 -07:00
Gleb Khmyznikov
6e6e637c5a Merge branch 'main' into gleb/advanced-paste 2026-06-16 15:41:44 -07:00
khmyznikov
05e4076929 remove unused word 2026-06-13 14:44:14 -07:00
khmyznikov
b123a4f14a fix wording 2026-06-13 14:10:42 -07:00
khmyznikov
4ce652945c Address PR review 2026-06-13 13:58:35 -07:00
khmyznikov
2710f0c599 fix winmd collision 2026-06-12 15:17:54 -07:00
khmyznikov
f6c8550039 some knowlegde updates 2026-06-12 14:58:37 -07:00
khmyznikov
54943c943e fix missing metadata 2026-06-12 14:05:27 -07:00
khmyznikov
714da1ab92 add unlock status 2026-06-12 09:42:36 -07:00
Gleb Khmyznikov
3768a9584c Merge branch 'main' into gleb/advanced-paste 2026-06-11 16:19:38 -07:00
khmyznikov
1729e26ce3 add model download trigger + winappsdk version fix 2026-06-11 14:41:40 -07:00
khmyznikov
2019d0b419 fix AI package version 2026-06-11 12:16:40 -07:00
khmyznikov
64071b7af0 add model prepare 2026-06-11 10:51:11 -07:00
khmyznikov
4d1171f7d0 fix AP location call 2026-06-11 00:10:44 -07:00
khmyznikov
ca3854744c ci: run cmdpal versioning setup last so CmdPalVersion matches stamped MSIX 2026-06-10 18:25:09 -07:00
khmyznikov
b63c319d83 Merge remote-tracking branch 'origin/main' into gleb/advanced-paste 2026-06-10 15:50:48 -07:00
khmyznikov
a7829722d3 Revert temp continueOnError on Touchdown step; service recovered 2026-06-10 15:50:40 -07:00
khmyznikov
c648980233 temp fix for resources 2026-06-10 13:59:45 -07:00
khmyznikov
a2ee84f285 fix xaml warnings 2026-06-10 11:13:18 -07:00
khmyznikov
faf0a7cc3f return Foundation version 2026-06-09 22:44:07 -07:00
khmyznikov
aa09683d96 revert nuget config 2026-06-09 19:36:35 -07:00
khmyznikov
2572b1217b bump to 2.2.2-experimental9 2026-06-09 18:29:12 -07:00
khmyznikov
96fb1d004b docs: fix stale PRI and path references in advancedpaste.md 2026-05-28 23:25:11 -07:00
khmyznikov
046796ae3b Merge branch 'gleb/advanced-paste' of https://github.com/microsoft/PowerToys into gleb/advanced-paste 2026-05-28 23:18:49 -07:00
khmyznikov
56af1e42eb Merge branch 'gleb/advanced-paste' of https://github.com/microsoft/PowerToys into gleb/advanced-paste 2026-05-28 23:18:22 -07:00
khmyznikov
5e0f92fbf2 Merge branch 'gleb/advanced-paste' of https://github.com/microsoft/PowerToys into gleb/advanced-paste 2026-05-28 23:14:58 -07:00
khmyznikov
ccbd288a68 Merge branch 'gleb/advanced-paste' of https://github.com/microsoft/PowerToys into gleb/advanced-paste
# Conflicts:
#	.pipelines/v2/templates/steps-build-installer-vnext.yml
#	installer/PowerToysSetupVNext/CmdPal.wxs
#	installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
#	src/CmdPalVersion.props
#	src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj
#	tools/Verification scripts/verify-installation-script.ps1
2026-05-28 23:14:05 -07:00
khmyznikov
e9221d2174 Merge branch 'gleb/advanced-paste' of https://github.com/microsoft/PowerToys into gleb/advanced-paste
# Conflicts:
#	.pipelines/v2/templates/steps-build-installer-vnext.yml
#	installer/PowerToysSetupVNext/CmdPal.wxs
#	installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
#	src/CmdPalVersion.props
#	src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj
#	tools/Verification scripts/verify-installation-script.ps1
2026-05-28 23:12:03 -07:00
Gleb Khmyznikov
e7e3db830f Merge branch 'main' into gleb/advanced-paste 2026-05-28 23:09:45 -07:00
Gleb Khmyznikov
cc55315092 Merge branch 'main' into gleb/advanced-paste 2026-05-28 23:02:56 -07:00
khmyznikov
6f981528de revert change 2026-05-28 23:00:09 -07:00
khmyznikov
450568a0e8 Revert generated installer wxs files and stale build artifacts
The generateAllFileComponents.ps1 output was accidentally committed, expanding all wxs placeholder comments into hardcoded file lists. Also removes stale CmdPal BuildInfo.xml/GeneratedPackage.appxmanifest and Directory.Build.props/targets that were generated by the local build-installer.ps1 script.
2026-05-28 22:57:40 -07:00
khmyznikov
9ea621b180 Bump WindowsAppSDK.Foundation to 2.0.22 (sparse PRI fix)
Requires WindowsAppSDK PR #6376 which fixes MRT PRI lookup under sparse package identity. Without this, Application.LoadComponent hard-codes resources.pri instead of respecting ProjectPriFileName, crashing any WinUI3 app with sparse identity and a custom PRI name.
2026-05-28 22:49:46 -07:00
khmyznikov
020dd67316 Revert PRI to PowerToys.AdvancedPaste.pri: resources.pri breaks Settings
Having resources.pri in the shared WinUI3Apps directory causes Settings and QuickAccess to crash (.NET CLR exception) because MRT picks up AP's resources.pri as a fallback for other apps' resource loading. The earlier XamlParseException was actually caused by a stale/broken sparse identity registration (ExternalLocation pointing nowhere), not the PRI name — confirmed by reproducing the same crash with Settings launched from the same dev build. ImageResizer successfully uses a custom PRI name under sparse identity, so AP should too.
2026-05-20 15:59:00 -07:00
khmyznikov
c6176fb436 AdvancedPaste: revert PRI to resources.pri for XAML LoadComponent
WinUI's Application.LoadComponent hard-codes 'resources.pri' lookup under sparse identity (WinAppSDK 2.0.1). Custom PRI names work for ResourceLoader (explicit path) but not for the XAML framework's built-in ms-appx:/// URI resolution. Reverts ProjectPriFileName to resources.pri and downgrades the audit check from error to warning.
2026-05-20 14:58:19 -07:00
khmyznikov
0f7c3f70d3 AdvancedPaste: pass PRI filename to ResourceLoader explicitly
Under sparse identity, ResourceLoader() defaults to 'resources.pri' which doesn't exist (our PRI is PowerToys.AdvancedPaste.pri). This caused ERROR_MRM_MAP_NOT_FOUND (0x80073B01) crashing XAML's MeasureOverride. Match ImageResizer's pattern: new ResourceLoader('PowerToys.AdvancedPaste.pri').
2026-05-20 12:52:51 -07:00
khmyznikov
ec475f0dcc to prev 2026-05-20 11:01:22 -07:00
khmyznikov
7a3539f6d8 Revert heat-based installer: AP is flat in WinUI3Apps like ImageResizer
AP on main already outputs flat to WinUI3Apps\ and is harvested by WinUI3ApplicationsFiles. Adding sparse identity (like ImageResizer) requires zero installer changes. Reverts the heat script, Product.wxs changes, and generateAllFileComponents.ps1 churn. Only non-main installer change is CmdPal.wxs fix (unrelated) and CmdPalPackagePath/Dir wixproj defines.
2026-05-20 10:58:23 -07:00
khmyznikov
f4ddb79707 Revert flatten: restore AP subfolder layout with heat harvest + ICE03 fix
Flattening AP into WinUI3Apps\ broke the installed build because the existing flat WinUI3ApplicationsFiles harvester doesn't recurse into subdirectories. AP's XBF files (AdvancedPasteXAML\), locale satellites, and arm64\ native DLLs were missing from the MSI. Restore the WinUI3Apps\AdvancedPaste\ subfolder layout with heat-based recursive harvest (like Monaco). Three CI fixes: 1) Language='0' on all heat File entries to prevent ICE03 on gd-GB/mi-NZ/ug-CN .mui files 2) ESRPSigning paths 3) Unique PRI name (PowerToys.AdvancedPaste.pri) passes audit.
2026-05-20 10:43:24 -07:00
khmyznikov
b2251cab7f wixproj: pass CmdPalPackagePath/CmdPalPackageDir defines to WiX
CmdPal.wxs references these preprocessor vars; reverting the wixproj to main dropped them. Restore the two CmdPal-only defines (without re-introducing the AdvancedPaste heat scaffolding).
2026-05-19 20:51:27 -07:00
khmyznikov
5931b3d569 ESRPSigning_core: drop AdvancedPaste subfolder path
AP binaries now live flat in WinUI3Apps\, matching every other unpackaged WinUI3 module.
2026-05-19 17:43:17 -07:00
khmyznikov
abcb77dea1 AdvancedPaste: rename ProjectPriFileName to PowerToys.AdvancedPaste.pri
After flattening AP into WinUI3Apps\, its 'resources.pri' collided with the WinUI3Apps root convention and tripped verifyPossibleAssetConflicts.ps1. Use the same per-module naming pattern as Settings/Peek/FileLocksmith/etc. The stale comment about sparse XAML needing 'resources.pri' was wrong -- Settings already runs sparse with PowerToys.Settings.pri.
2026-05-19 16:11:05 -07:00
khmyznikov
7da7a0b277 AdvancedPaste: flatten install layout to WinUI3Apps root
AP was the only WinUI3 module shipping into a WinUI3Apps\AdvancedPaste subfolder, which forced a separate heat-based harvest in the installer. Move output to WinUI3Apps\ alongside Settings/Hosts/Peek so the existing WinUI3ApplicationsFiles harvester + dedup/CreateWinAppSDKHardlinksCA path covers AP for free. Reverts the installer scaffolding (heat script, Product.wxs/wixproj edits, generateAllFileComponents.ps1 churn, CustomAction.cpp + WinUI3Applications.wxs hardlinks rollback) so the diff is back to what main does for every other unpackaged WinUI3 app. Also drops the stale subfolder reference in verify-installation-script.ps1 and the sparse AppxManifest Executable path.
2026-05-19 15:01:09 -07:00
khmyznikov
6d1c414b07 fix vnext 2026-05-19 13:22:12 -07:00
khmyznikov
279aaf6035 fix the pipeline 2026-05-19 11:15:00 -07:00
khmyznikov
125fffc607 fix vnext 2026-05-18 22:56:32 -07:00
khmyznikov
0fcd72920d revert the CI changes 2026-05-18 19:33:46 -07:00
khmyznikov
361959374b get back to sparse 2026-05-18 16:15:34 -07:00
khmyznikov
17a0533d08 fix ap tests 2026-05-11 22:36:57 -07:00
khmyznikov
d3c8b4833c fix spelling 2026-05-11 21:25:41 -07:00
khmyznikov
832edcd580 fix vnext 2026-05-11 18:12:34 -07:00
khmyznikov
c2caa46a3f to prev 2026-05-11 15:18:45 -07:00
khmyznikov
6657da8310 move to wasdk 2.0 2026-05-11 14:55:54 -07:00
khmyznikov
79f850b0c5 Merge remote-tracking branch 'origin/main' into gleb/advanced-paste
# Conflicts:
#	Directory.Packages.props
#	src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props
2026-05-11 10:32:43 -07:00
khmyznikov
8370908d75 build fix on VS2026 2026-05-10 14:24:26 -07:00
khmyznikov
ca7bc16b7a to prev 2026-05-10 14:24:15 -07:00
khmyznikov
7416209cc7 address PR review 2026-05-09 14:57:27 -07:00
Gleb Khmyznikov
3269de6172 Merge branch 'main' into gleb/advanced-paste 2026-05-08 14:30:55 -07:00
Gleb Khmyznikov
96c5d16a45 Fix AP uninstall 2026-04-01 11:10:23 -07:00
Gleb Khmyznikov
9e02f485f0 fix debug tokens 2026-03-31 16:24:45 -07:00
Gleb Khmyznikov
822303a68c fix LAF tokens 2026-03-31 15:56:57 -07:00
Gleb Khmyznikov
68eb695626 maybe final fix? 2026-03-30 17:00:55 -07:00
Gleb Khmyznikov
d776b64a64 another thing 2026-03-30 14:41:20 -07:00
Gleb Khmyznikov
dbae67dfaa Fixing AP crash 2026-03-30 12:18:22 -07:00
Gleb Khmyznikov
011fc5a190 exclude AP from audit 2026-03-29 21:55:59 -07:00
Gleb Khmyznikov
d628d73e0b AP DisableTransitiveFrameworkReferences 2026-03-29 19:50:32 -07:00
Gleb Khmyznikov
3281c0e1a5 fix missing VCLibs 2026-03-27 23:17:18 -07:00
Gleb Khmyznikov
749378d3a2 to prev 3 2026-03-27 20:21:22 -07:00
Gleb Khmyznikov
00bc7ec822 to prev 2 2026-03-27 14:01:20 -07:00
Gleb Khmyznikov
faae1ae694 to prev 2026-03-27 09:30:40 -07:00
Gleb Khmyznikov
c43261f7cc keep fixing msix 2026-03-27 08:35:00 -07:00
Gleb Khmyznikov
ce8508ef97 fix ap vnext 2026-03-26 16:16:19 -07:00
Gleb Khmyznikov
ab9a8c979c fix ap msix search 2026-03-26 14:52:42 -07:00
Gleb Khmyznikov
9befd280a2 another try 3 2026-03-26 14:02:05 -07:00
Gleb Khmyznikov
7436b46fd6 another try 2 2026-03-26 13:18:36 -07:00
Gleb Khmyznikov
3dfeeceb7d Merge branch 'main' into gleb/advanced-paste 2026-03-26 11:16:04 -07:00
Gleb Khmyznikov
26bbfdeb8f another try 2026-03-26 11:14:23 -07:00
Gleb Khmyznikov
c46e6147b5 fix build 2026-03-26 10:32:02 -07:00
Gleb Khmyznikov
36366e13c9 fix msix 2026-03-25 16:39:34 -07:00
Gleb Khmyznikov
4fac403966 debug build 2026-03-25 15:00:15 -07:00
Gleb Khmyznikov
d28ae33c22 fix ap package 2026-03-25 13:44:14 -07:00
Gleb Khmyznikov
401ddcd95e move AP to packaged identity 2026-03-24 16:55:19 -07:00
Gleb Khmyznikov
0bc45045e0 Merge branch 'gleb/advanced-paste' of https://github.com/microsoft/PowerToys into gleb/advanced-paste 2026-03-23 21:42:10 -07:00
Gleb Khmyznikov
03438406c7 fix installer 2026-03-23 21:42:06 -07:00
gkhmyznikov
9f6d6b9cf2 Fix build 2026-03-23 16:34:54 -07:00
gkhmyznikov
f53e64a456 fix xaml 2026-03-23 15:17:27 -07:00
gkhmyznikov
0a2bc81c25 fix laf attestation string 2026-03-23 14:47:55 -07:00
Gleb Khmyznikov
fd96b463c4 Add laf as ADO secret 2026-03-21 23:26:33 -07:00
Gleb Khmyznikov
9421efe015 update readme 2026-03-21 22:44:53 -07:00
Gleb Khmyznikov
32ffd3a3b2 change workaround 2026-03-21 22:18:45 -07:00
Gleb Khmyznikov
a60eab4d03 add more params from sample 2026-03-21 14:44:46 -07:00
Gleb Khmyznikov
c2789d6080 launch hacks 2026-03-20 23:04:49 -07:00
Gleb Khmyznikov
8224062c3b add workaround 2026-03-19 17:04:56 -07:00
gkhmyznikov
dc1484549a Add LAF helper 2026-03-19 14:13:12 -07:00
gkhmyznikov
51c5d320f3 upgrade to 260317003 2026-03-19 13:50:22 -07:00
gkhmyznikov
b5fc9fea78 Revert "WinAppSdk 2.0 try"
This reverts commit a25ce4ed02.
2026-03-19 13:47:03 -07:00
gkhmyznikov
a25ce4ed02 WinAppSdk 2.0 try 2026-03-19 12:31:28 -07:00
gkhmyznikov
a3620f1d5e add settings ui manifest 2026-03-18 17:14:08 -07:00
gkhmyznikov
f8b158659e Phi Silica not tested 2026-03-17 15:15:41 -07:00
gkhmyznikov
a025d4dfc4 add settings collapse 2026-03-12 17:26:52 -07:00
gkhmyznikov
f6149bb72f default provider 2026-03-12 16:37:42 -07:00
gkhmyznikov
74eafb56b3 Custom models picker, default models picker 2026-03-12 16:13:15 -07:00
gkhmyznikov
6072ec7d8f fix settings buttons 2026-03-11 18:51:07 -07:00
gkhmyznikov
1c78950d83 more improvements 2026-03-11 18:24:34 -07:00
gkhmyznikov
ecc93eaa33 more customization 2026-03-11 15:18:51 -07:00
gkhmyznikov
eed0e20f6b fix shortcut 2026-03-10 14:44:43 -07:00
gkhmyznikov
a48e741a40 initial 2026-03-10 13:14:15 -07:00
56 changed files with 2177 additions and 125 deletions

View File

@@ -309,6 +309,11 @@ pwa
AOT
Aot
ify
LAF
Laf
languagemodel
philm
phisilica
TFM
# YML

View File

@@ -1602,7 +1602,6 @@ sancov
SAVEFAILED
schedtasks
SCID
SCL
Scode
SCREENFONTS
screenruler

View File

@@ -313,6 +313,9 @@ ms-windows-store://\S+
# ANSI color codes
(?:\\(?:u00|x)1[Bb]|\\03[1-7]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+)*m
# Phi Silica internal token/ID literals
<PhiSilicaLafToken\b[^>]*>[^<]+</PhiSilicaLafToken>
\bdjwsxzxb4ksa8\b
# Special licenses text from RNNoise (BSD-style disclaimer: ``AS IS'')
``AS IS''

View File

@@ -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:
# Install the Terrapin retrieval tool, which replaces vcpkg's download handler
# to redirect it to a safe Microsoft-controlled location

View File

@@ -266,6 +266,17 @@ jobs:
VCWhereExtraVersionTarget: '-prerelease'
- ${{ if eq(parameters.official, true) }}:
# M.W.T.V Setup.ps1 sets the pipeline-level XES_APPXMANIFESTVERSION env var
# from the supplied -ProjectDirectory's custom.props. Whichever invocation
# runs last wins. cmdpal MUST run last because $(CmdPalVersion) (used by the
# VNext installer and CmdPal's AppxPackageTestDir) falls back to that env
# var; if AP wins, CmdPal's folder name and MSIX filename disagree on
# VersionMinor and the installer fails with WIX0103. Each project's own
# custom.props is still applied at MSBuild time, so AP versioning is
# unaffected by the order.
- template: .\steps-setup-versioning.yml
parameters:
directory: $(build.sourcesdirectory)\src\modules\AdvancedPaste
- template: .\steps-setup-versioning.yml
parameters:
directory: $(build.sourcesdirectory)\src\modules\cmdpal

View File

@@ -3,6 +3,7 @@
<RepoRoot>$(MSBuildThisFileDirectory)</RepoRoot>
</PropertyGroup>
<Import Project="$(RepoRoot)src\Version.props" />
<Import Project="$(RepoRoot)src\PhiSilicaLaf.props" />
<PropertyGroup>
<Copyright>Copyright (C) Microsoft Corporation. All rights reserved.</Copyright>
<AssemblyCopyright>Copyright (C) Microsoft Corporation. All rights reserved.</AssemblyCopyright>

View File

@@ -0,0 +1,100 @@
# Advanced Paste Phi Silica local testing
How to build, register, and test **Phi Silica** in **Advanced Paste (AP)** on a dev machine,
plus the few things that actually break it.
## How it fits together
AP ships as an **unpackaged, self-contained WinUI 3 exe** (`PowerToys.AdvancedPaste.exe`).
The Windows AI `LanguageModel` (Phi Silica) API is a **Limited Access Feature (LAF)**. For it
to work, all of these must line up:
1. **Package identity** — AP runs with identity granted by the sparse MSIX
`Microsoft.PowerToys.SparseApp`.
2. **Matching LAF creds** — the token/attestation baked into the exe match the registered
sparse package's publisher.
3. **AI metadata deployed** — the `Microsoft.Windows.AI*.winmd` files ship next to the exe;
the AI runtime resolves them **at runtime**.
4. **Model ready** — supported hardware and the on-device model downloaded
(`GetReadyState() == Ready`).
Two identities — the baked token must match the registered package's publisher:
| Build | Publisher Id | LAF creds |
|-------|--------------|-----------|
| **Dev** | `djwsxzxb4ksa8` | dev default in [`src/PhiSilicaLaf.props`](../../../src/PhiSilicaLaf.props) |
| **Prod** | `8wekyb3d8bbwe` | secret, injected only by `.pipelines/v2/release.yml` |
Non-secret pairing check: the exe's baked **Attestation** must equal the registered package's
**PublisherId**.
## Build + register (dev loop)
```powershell
$repo = "X:\GitHub\PowerToys"; $Plat = "ARM64"; $Cfg = "Debug" # or x64 / Release
# Build AP only (C#; reuses existing C++ outputs):
dotnet restore "$repo\src\modules\AdvancedPaste\AdvancedPaste\AdvancedPaste.csproj" /p:Platform=$Plat
& "$repo\tools\build\build.cmd" -Path "$repo\src\modules\AdvancedPaste\AdvancedPaste" `
-Platform $Plat -Configuration $Cfg /p:BuildProjectReferences=false
# Register the dev sparse package (creates + trusts a dev cert, grants identity):
pwsh -ExecutionPolicy Bypass -File "$repo\src\PackageIdentity\BuildSparsePackage.ps1" `
-Platform $Plat -Configuration $Cfg -DevRegister
# Expect: PublisherId djwsxzxb4ksa8, IsDevelopmentMode True
```
## Check the API
`PowerToys.AdvancedPaste.exe` is a **GUI-subsystem** app — run directly in a console it prints
nothing and returns no exit code. **Redirect** stdout/stderr and wait:
```powershell
$exe = "$repo\$Plat\$Cfg\WinUI3Apps\PowerToys.AdvancedPaste.exe"
$o = "$env:TEMP\ap.out"; $e = "$env:TEMP\ap.err"
$p = Start-Process $exe '--check-phi-silica' -Wait -PassThru -WindowStyle Hidden `
-RedirectStandardOutput $o -RedirectStandardError $e
"exit=$($p.ExitCode) stdout=$((Get-Content $o -Raw).Trim())"
Get-Content $e -Raw # stderr: [phi-silica] LAF unlock status: <…>; ReadyState: <…>
```
| `--check-phi-silica` | `--prepare-phi-silica` (downloads the model) |
|----------------------|----------------------------------------------|
| `0` Available · `1` NotReady · `2` NotSupported / unlock failed | `0` Ready · `1` Failed · `2` NotSupported |
`--check` only reads state; use `--prepare` to trigger the model download (`EnsureReadyAsync`).
On failure it prints the `HRESULT` to stderr.
Confirm the running AP has identity:
```powershell
$apPid = (Get-Process PowerToys.AdvancedPaste -EA SilentlyContinue | Select-Object -First 1).Id
if ($apPid) { & "$repo\src\PackageIdentity\Check-ProcessIdentity.ps1" -ProcessId $apPid }
# Expect a PFN ending in the publisher id that matches the baked attestation
```
## What actually breaks it
- **Missing `.winmd` (most important).** The Windows AI runtime resolves
`Microsoft.Windows.AI*.winmd` from the app folder at runtime. If they aren't deployed,
`GetReadyState()` returns `NotReady` and `EnsureReadyAsync()` fails with
`RO_E_METADATA_NAME_NOT_FOUND` (`0x8000000F`) — even though identity, token, and the AI DLLs
are all correct. The build emits these winmd into `WinUI3Apps\`; the **installer must harvest
them** (`*.winmd` is in the inclusion list of
[`generateAllFileComponents.ps1`](../../../installer/PowerToysSetupVNext/generateAllFileComponents.ps1)).
Classic symptom: "works from the build output but not from the installer" → check that the
installed `WinUI3Apps\` contains `Microsoft.Windows.AI*.winmd`.
- **Dev/prod mismatch.** A dev-cred exe running against a prod sparse package (or vice versa)
makes the LAF unlock silently return `Unavailable`. Keep the exe and the registered package
the same flavor, and verify with the attestation == publisherId check above.
- **Forgot to redirect.** `--check-phi-silica` in a console prints nothing — that's the
GUI-subsystem quirk, not a result.
## Cleanup
```powershell
pwsh -ExecutionPolicy Bypass -File "$repo\src\PackageIdentity\BuildSparsePackage.ps1" -Unregister
```
⚠️ This removes any `Microsoft.PowerToys.SparseApp` registration, **including a prod one** from
an installer — reinstall/repair PowerToys to restore it.

View File

@@ -33,7 +33,81 @@ See the `ExecutePasteFormatAsync(PasteFormat, PasteActionSource)` method in `Opt
## Debugging
TODO: Add debugging information
Advanced Paste is an unpackaged, self-contained WinUI 3 app (`PowerToys.AdvancedPaste.exe`). To call Windows AI APIs (Phi Silica / `Microsoft.Windows.AI.Text.LanguageModel`) it acquires **package identity** at runtime via a shared sparse MSIX package (`Microsoft.PowerToys.SparseApp`).
### 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.
### Sparse package identity (local development)
#### Why is this needed?
- The `LanguageModel` API requires a Limited Access Feature (LAF) unlock, which only succeeds when the calling process has a matching package identity.
- Advanced Paste is an unpackaged, self-contained WinUI 3 app. The sparse package grants it identity without converting it to a full MSIX.
- The csproj uses `<ProjectPriFileName>PowerToys.AdvancedPaste.pri</ProjectPriFileName>` (matching the convention of other WinUI3 apps like ImageResizer). This requires WindowsAppSDK Foundation >= 2.0.22 ([PR #6376](https://github.com/microsoft/WindowsAppSDK/pull/6376)) which fixes MRT PRI lookup under sparse identity so `Application.LoadComponent` resolves custom-named PRI files instead of hard-coding `resources.pri`.
#### One-step dev setup
```powershell
pwsh src/PackageIdentity/BuildSparsePackage.ps1 -Platform ARM64 -Configuration Debug -DevRegister
```
`-DevRegister`:
1. Generates a dev certificate under `src/PackageIdentity/.user/` (first run only).
2. Auto-imports that certificate into `CurrentUser\TrustedPeople` and `CurrentUser\Root` so the OS grants sparse identity to AP (without trust, `GetPackageFamilyName` returns `APPMODEL_ERROR_NO_PACKAGE` and LAF unlock silently fails).
3. Removes any prior registration.
4. Rewrites the publisher in a temp copy of `AppxManifest.xml` to match the dev cert subject.
5. Registers via `Add-AppxPackage -Register … -ExternalLocation X:\…\<Platform>\<Config>\WinUI3Apps`.
After registration verify:
```powershell
$pkg = Get-AppxPackage -Name '*SparseApp*'
$pkg.PackageFamilyName # Microsoft.PowerToys.SparseApp_<PublisherId>
$pkg.PublisherId # djwsxzxb4ksa8
$pkg.IsDevelopmentMode # True
```
Confirm AP picks up sparse identity at runtime:
```powershell
& 'ARM64\Debug\WinUI3Apps\PowerToys.AdvancedPaste.exe' --check-phi-silica
# Exit 0 = Available, 1 = NotReady, 2 = NotSupported
```
Re-register after rebuilding AP, changing `src/PackageIdentity/AppxManifest.xml`, or switching platforms/configurations by re-running the same command. Unregister with `-Unregister`.
#### Troubleshooting
| Problem | Cause | Fix |
|---------|-------|-----|
| `GetPackageFamilyName` returns `APPMODEL_ERROR_NO_PACKAGE` (15700) at runtime; LAF unlock returns `Unavailable` | Dev certificate not trusted (or sparse package not registered) | Re-run `BuildSparsePackage.ps1 -DevRegister` — auto-imports the cert into `TrustedPeople` and `Root`. |
| `Microsoft.UI.Xaml.dll` crash with `0xC000027B` (class-not-registered) on AP or Settings startup | `<Application>` `Executable` path in `src/PackageIdentity/AppxManifest.xml` does not resolve under the registered `ExternalLocation` (`<Config>\WinUI3Apps\`) | Confirm every `Executable` is relative to `WinUI3Apps\` (per #47177) and the file exists under the build output. |
| AP launches but never shows a window when triggered via hotkey | Runner's pipe-server wait timed out before AP's cold-start finished bootstrapping WinAppSDK + DI host | Already mitigated by the 15 s pipe timeout in `AdvancedPasteProcessManager.cpp`; warm-start launches connect in well under 1 s. |
| `XamlParseException` / `ms-appx:///Microsoft.UI.Xaml/Themes/…` not found | WindowsAppSDK Foundation < 2.0.22; MRT can't resolve custom PRI name under sparse identity | Ensure `Microsoft.WindowsAppSDK.Foundation` >= 2.0.22 in `Directory.Packages.props`. |
### How Settings UI checks Phi Silica availability
Settings UI does not have sparse package identity. To check whether Phi Silica is available, it launches Advanced Paste as a short-lived subprocess:
```
PowerToys.AdvancedPaste.exe --check-phi-silica
```
`Program.Main` recognizes this flag, calls `PhiSilicaLafHelper.TryUnlock()` + `LanguageModel.GetReadyState()`, prints one of `Available` / `NotReady` / `NotSupported` to stdout, and exits with the matching code (0/1/2). Settings reads stdout with a 10 s wait. Because each call is a fresh process, transient `Unavailable` results are not cached across checks.
### See also
- [Phi Silica local testing & troubleshooting guide](advancedpaste-phisilica-local-testing.md) — layer-by-layer diagnostics for Phi Silica availability
- [`src/PackageIdentity/readme.md`](/src/PackageIdentity/readme.md) — full sparse package documentation
- [microsoft/microsoft-ui-xaml#10856](https://github.com/microsoft/microsoft-ui-xaml/issues/10856) — original WinUI sparse-identity PRI bug
- [microsoft/WindowsAppSDK#6376](https://github.com/microsoft/WindowsAppSDK/pull/6376) — MRT sparse PRI fix (Foundation >= 2.0.22)
## Settings

View File

@@ -28,12 +28,23 @@ Function Generate-FileList() {
$fileExclusionList = @("*.pdb", "*.lastcodeanalysissucceeded", "createdump.exe", "powertoys.exe")
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri", "*.yml")
# *.winmd: WinRT metadata for the Windows App SDK AI APIs (Phi Silica, Imaging, etc.). The AI
# runtime resolves these from the app directory at runtime, so they must ship with the product.
# Without them GetReadyState() reports NotReady and EnsureReadyAsync() fails with
# RO_E_METADATA_NAME_NOT_FOUND (0x8000000F). The build already emits them into the app output
# (e.g. WinUI3Apps); they were previously dropped here because the harvest didn't include them.
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri", "*.yml", "*.winmd")
# MFC DLLs leak into the output via WindowsAppSDKSelfContained but no PowerToys binary imports them.
# Verified with dumpbin /dependents across all 2176 binaries — zero consumers.
$fileExclusionList += @("mfc140.dll", "mfc140u.dll", "mfcm140.dll", "mfcm140u.dll")
# Microsoft.CommandPalette.Extensions.winmd already has a dedicated WiX component
# (Microsoft_CommandPalette_Extensions_winmd in BaseApplications.wxs, placed in WinUI3Apps for
# CmdPal's WinRT resolution). Exclude it from the generic *.winmd harvest so it isn't declared
# by two components (WIX ICE30 "installed by two different components" breaks ref-counting).
$fileExclusionList += @("Microsoft.CommandPalette.Extensions.winmd")
$dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll")
if ($fileDepsJson -eq [string]::Empty) {

View File

@@ -58,6 +58,16 @@
AppListEntry="none">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.AdvancedPasteUI" Executable="PowerToys.AdvancedPaste.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="PowerToys.AdvancedPaste"
Description="PowerToys Advanced Paste"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png"
AppListEntry="none">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.CmdPalExtension" Executable="Microsoft.CmdPal.Ext.PowerToys.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="PowerToys.CommandPaletteExtension"

View File

@@ -13,7 +13,9 @@ Param(
[switch]$Clean,
[switch]$ForceCert,
[switch]$NoSign,
[switch]$CIBuild
[switch]$CIBuild,
[switch]$DevRegister,
[switch]$Unregister
)
# PowerToys sparse packaging helper.
@@ -22,6 +24,19 @@ Param(
$ErrorActionPreference = 'Stop'
# Handle -Unregister early exit
if ($Unregister) {
$existing = Get-AppxPackage -Name 'Microsoft.PowerToys.SparseApp' -ErrorAction SilentlyContinue
if ($existing) {
Write-Host "Removing Microsoft.PowerToys.SparseApp..."
$existing | Remove-AppxPackage
Write-Host "Done."
} else {
Write-Host "Microsoft.PowerToys.SparseApp is not registered."
}
exit 0
}
$isCIBuild = $false
if ($CIBuild.IsPresent) {
$isCIBuild = $true
@@ -421,3 +436,85 @@ $winUI3AppsDir = Join-Path $outDir "WinUI3Apps"
Write-BuildLog "Register sparse package:" -Level Info
Write-BuildLog " Add-AppxPackage -Path `"$msixPath`" -ExternalLocation `"$winUI3AppsDir`"" -Level Warning
Write-BuildLog "(If already installed and you changed manifest only): Add-AppxPackage -Register `"$manifestPath`" -ExternalLocation `"$winUI3AppsDir`" -ForceApplicationShutdown" -Level Warning
# -DevRegister: automatically register the sparse package for local development
if ($DevRegister) {
Write-BuildLog "`nDev registration requested..." -Level Info
# Ensure the dev certificate is trusted (CurrentUser\TrustedPeople + CurrentUser\Root).
# Without trust, sparse identity lookup silently fails at runtime (GetPackageFamilyName
# returns APPMODEL_ERROR_NO_PACKAGE) so LAF unlocks like Phi Silica return Unavailable.
if (Test-Path $CertCerFile) {
try {
$devCert = (Get-PfxCertificate -FilePath $CertCerFile -ErrorAction Stop)
$devThumbprint = $devCert.Thumbprint
foreach ($store in @('TrustedPeople', 'Root')) {
$storePath = "Cert:\CurrentUser\$store"
$present = Get-ChildItem $storePath -ErrorAction SilentlyContinue |
Where-Object { $_.Thumbprint -eq $devThumbprint }
if (-not $present) {
Write-BuildLog "Importing dev certificate ($devThumbprint) into CurrentUser\$store" -Level Info
Import-Certificate -FilePath $CertCerFile -CertStoreLocation $storePath | Out-Null
}
}
} catch {
Write-BuildLog "Failed to import dev certificate into trust stores: $($_.Exception.Message)" -Level Warning
Write-BuildLog "Sparse identity may not be granted at runtime; LAF unlocks (e.g., Phi Silica) will fail." -Level Warning
}
} else {
Write-BuildLog "Certificate .cer file not found at $CertCerFile; skipping trust-store import." -Level Warning
}
# Remove existing registration
$existing = Get-AppxPackage -Name $script:Config.IdentityName -ErrorAction SilentlyContinue
if ($existing) {
Write-BuildLog "Removing existing registration..." -Level Info
$existing | Remove-AppxPackage
}
# Create a temp manifest with the dev publisher for -Register
$devRegDir = Join-Path ([System.IO.Path]::GetTempPath()) "PowerToysSparseDevReg"
if (Test-Path $devRegDir) { Remove-Item $devRegDir -Recurse -Force }
New-Item -ItemType Directory -Path $devRegDir -Force | Out-Null
try {
Copy-Item $manifestPath (Join-Path $devRegDir 'AppxManifest.xml')
$imagesDir = Join-Path $sparseDir 'Images'
if (Test-Path $imagesDir) {
Copy-Item $imagesDir (Join-Path $devRegDir 'Images') -Recurse
}
$devManifest = Join-Path $devRegDir 'AppxManifest.xml'
[xml]$devXml = Get-Content $devManifest -Raw
$devIdentity = $devXml.Package.Identity
if ($devIdentity.Publisher -ne $script:Config.CertSubject) {
Write-BuildLog "Rewriting publisher for dev registration" -Level Info
$devIdentity.SetAttribute('Publisher', $script:Config.CertSubject)
$devXml.Save($devManifest)
}
Write-BuildLog "Registering with ExternalLocation: $winUI3AppsDir" -Level Info
Add-AppxPackage -Register $devManifest -ExternalLocation $winUI3AppsDir
$pkg = Get-AppxPackage -Name $script:Config.IdentityName -ErrorAction SilentlyContinue
if ($pkg) {
Write-BuildLog "Dev registration successful:" -Level Success
Write-BuildLog " Publisher: $($pkg.Publisher)" -Level Info
Write-BuildLog " PublisherId: $($pkg.PublisherId)" -Level Info
Write-BuildLog " IsDevelopmentMode: $($pkg.IsDevelopmentMode)" -Level Info
Write-BuildLog " InstallLocation: $($pkg.InstallLocation)" -Level Info
if ($pkg.PublisherId -ne 'djwsxzxb4ksa8') {
Write-BuildLog "PublisherId mismatch! Expected 'djwsxzxb4ksa8', got '$($pkg.PublisherId)'. LAF unlock will fail." -Level Warning
}
} else {
Write-BuildLog "Dev registration failed — package not found." -Level Error
exit 1
}
} finally {
if (Test-Path $devRegDir) {
Remove-Item $devRegDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
}

9
src/PhiSilicaLaf.props Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- Phi Silica Limited Access Feature credentials.
Local dev defaults below; overridden by /p: in release pipelines. -->
<PhiSilicaLafToken Condition="'$(PhiSilicaLafToken)'==''">RmToMMYJHZkQSrKP5lWesA==</PhiSilicaLafToken>
<PhiSilicaLafAttestation Condition="'$(PhiSilicaLafAttestation)'==''">djwsxzxb4ksa8</PhiSilicaLafAttestation>
</PropertyGroup>
</Project>

View File

@@ -55,6 +55,22 @@ internal sealed class IntegrationTestUserSettings : IUserSettings
public IReadOnlyList<PasteFormats> 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;

View File

@@ -30,6 +30,16 @@ public sealed class CustomActionKernelQueryCacheServiceTests
private static readonly CacheValue TestValue = new([new(PasteFormats.PlainText, [])]);
private static readonly CacheValue TestValue2 = new([new(PasteFormats.KernelQuery, new() { { "a", "b" }, { "c", "d" } })]);
private static string LocalizeResourceId(string resourceId) => resourceId switch
{
"PasteAsPlainText" => "Paste as plain text",
"PasteAsMarkdown" => MarkdownTestKey.Prompt,
"PasteAsJson" => JSONTestKey.Prompt,
"PasteAsTxtFile" => PasteAsTxtFileKey.Prompt,
"PasteAsPngFile" => PasteAsPngFileKey.Prompt,
_ => resourceId,
};
private CustomActionKernelQueryCacheService _cacheService;
private Mock<IUserSettings> _userSettings;
private MockFileSystem _fileSystem;
@@ -41,7 +51,7 @@ public sealed class CustomActionKernelQueryCacheServiceTests
UpdateUserActions([], []);
_fileSystem = new();
_cacheService = new(_userSettings.Object, _fileSystem);
_cacheService = new(_userSettings.Object, _fileSystem, LocalizeResourceId);
}
[TestMethod]
@@ -122,7 +132,7 @@ public sealed class CustomActionKernelQueryCacheServiceTests
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, LocalizeResourceId); // recreate using same mock file-system to simulate app restart
AssertAreEqual(TestValue, _cacheService.ReadOrNull(JSONTestKey));
AssertAreEqual(TestValue2, _cacheService.ReadOrNull(MarkdownTestKey));

View File

@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\CmdPalVersion.props" />
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
@@ -8,7 +9,7 @@
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
<UseWinUI>true</UseWinUI>
<ApplicationIcon>Assets\AdvancedPaste\AdvancedPaste.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationManifest>AdvancedPaste.dev.manifest</ApplicationManifest>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
@@ -20,9 +21,13 @@
<RootNamespace>AdvancedPaste</RootNamespace>
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>PowerToys.AdvancedPaste.pri</ProjectPriFileName>
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
<Version>$(AdvancedPasteVersion)</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(CIBuild)'=='true'">
<ApplicationManifest>AdvancedPaste.prod.manifest</ApplicationManifest>
</PropertyGroup>
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
@@ -32,6 +37,25 @@
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>
<!-- BODGY: XES Versioning and WinAppSDK get into a fight about the app manifest, which breaks WinAppSDK. -->
<Target Name="RearrangeXefVersioningAndWinAppSDKResourceGeneration" DependsOnTargets="GetNewAppManifestValues;CreateWinRTRegistration" BeforeTargets="XesWriteVersionInfoResourceFile">
<PropertyGroup>
<OriginalApplicationManifest>$(ApplicationManifest.Replace("$(MSBuildProjectDirectory)\",""))</OriginalApplicationManifest>
</PropertyGroup>
<Message Importance="High" Text="Updated final manifest path to $(OriginalApplicationManifest)" />
</Target>
<!-- Generate PhiSilicaLafCredentials from MSBuild properties (PhiSilicaLaf.props / CI overrides). -->
<Target Name="GeneratePhiSilicaLafCredentials" BeforeTargets="BeforeCompile;CoreCompile;XamlPreCompile">
<PropertyGroup>
<PhiSilicaLafCredentialsFile>$(IntermediateOutputPath)PhiSilicaLafCredentials.g.cs</PhiSilicaLafCredentialsFile>
</PropertyGroup>
<WriteLinesToFile File="$(PhiSilicaLafCredentialsFile)" Overwrite="true" Lines="// &lt;auto-generated/&gt;;namespace AdvancedPaste%3B;;internal static class PhiSilicaLafCredentials;{; internal const string Token = &quot;$(PhiSilicaLafToken)&quot;%3B; internal const string Attestation = &quot;$(PhiSilicaLafAttestation)&quot;%3B;}" />
<ItemGroup>
<Compile Include="$(PhiSilicaLafCredentialsFile)" Condition="!@(Compile->AnyHaveMetadataValue('Identity', '$(PhiSilicaLafCredentialsFile)'))" />
</ItemGroup>
</Target>
<ItemGroup>
<None Remove="AdvancedPasteXAML\Controls\ClipboardHistoryItemPreviewControl.xaml" />
<None Remove="AdvancedPasteXAML\Controls\PromptBox.xaml" />
@@ -66,9 +90,9 @@
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.SemanticKernel" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.ClientModel" />
<PackageReference Include="Microsoft.Windows.Compatibility" />
<PackageReference Include="Microsoft.Windows.CsWin32" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="ReverseMarkdown" />
@@ -85,7 +109,8 @@
<!-- TODO: fix issues and reenable -->
<!-- These are caused by streamjsonrpc dependency on Microsoft.VisualStudio.Threading.Analyzers -->
<!-- We might want to add that to the project and fix the issues as well -->
<NoWarn>VSTHRD002;VSTHRD110;VSTHRD100;VSTHRD200;VSTHRD101</NoWarn>
<!-- CS8305: generated XamlTypeInfo references experimental ItemsView enums in WinAppSDK 2.2.x-experimental. -->
<NoWarn>VSTHRD002;VSTHRD110;VSTHRD100;VSTHRD200;VSTHRD101;CS8305</NoWarn>
</PropertyGroup>
<!--
@@ -97,6 +122,10 @@
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<SDKReference Include="Microsoft.VCLibs.Desktop, Version=14.0" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>AdvancedPaste.UnitTests</_Parameter1>
@@ -104,8 +133,6 @@
</ItemGroup>
<ItemGroup>
<!-- HACK: Common.UI is referenced, even if it is not used, to force dll versions to be the same as in other projects that use it. It's still unclear why this is the case, but this is need for flattening the install directory. -->
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\LanguageModelProvider\LanguageModelProvider.csproj" />
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
@@ -154,4 +181,5 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0"
xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="AdvancedPaste.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
publisher="CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US"
packageName="Microsoft.PowerToys.SparseApp"
applicationId="PowerToys.AdvancedPasteUI" />
</assembly>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0"
xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="AdvancedPaste.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
packageName="Microsoft.PowerToys.SparseApp"
applicationId="PowerToys.AdvancedPasteUI" />
</assembly>

View File

@@ -188,14 +188,23 @@ namespace AdvancedPaste
}
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);
}
}
}

View File

@@ -382,6 +382,7 @@
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid
@@ -438,13 +439,32 @@
TextWrapping="Wrap" />
</ScrollViewer>
</Grid>
<Rectangle
<StackPanel
Grid.Row="1"
Padding="12,8,12,8"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
Spacing="4"
Visibility="{x:Bind ViewModel.HasCoachingExplanation, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock
x:Uid="CoachingExplanationTitle"
FontWeight="SemiBold"
Style="{StaticResource CaptionTextBlockStyle}" />
<ScrollViewer MaxHeight="200">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.CoachingExplanation, Mode=OneWay}"
TextWrapping="Wrap" />
</ScrollViewer>
</StackPanel>
<Rectangle
Grid.Row="2"
Height="1"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
<Grid
Grid.Row="2"
Grid.Row="3"
Margin="12"
ColumnSpacing="8">
<Grid.ColumnDefinitions>

View File

@@ -25,6 +25,22 @@ namespace AdvancedPaste.Settings
public IReadOnlyList<PasteFormats> 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;

View File

@@ -157,8 +157,6 @@ namespace AdvancedPaste.Helpers
{
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")]

View File

@@ -46,6 +46,22 @@ namespace AdvancedPaste.Settings
public IReadOnlyList<AdvancedPasteCustomAction> 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 @@ namespace AdvancedPaste.Settings
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]),

View File

@@ -24,20 +24,22 @@ public sealed class PasteFormat
IsEnabled = SupportsClipboardFormats(clipboardFormats) && (isAIServiceEnabled || !Metadata.RequiresAIService);
}
public static PasteFormat CreateStandardFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, Func<string, string> resourceLoader) =>
public static PasteFormat CreateStandardFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, Func<string, string> 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 sealed class PasteFormat
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; }

View File

@@ -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",

View File

@@ -0,0 +1,67 @@
// 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 _unlocked;
/// <summary>
/// Gets the status of the most recent <see cref="TryUnlock"/> attempt
/// (e.g. Available, AvailableWithoutToken, Unavailable, or "Exception: ...").
/// Exposed so callers can surface the real LAF result for diagnostics; the
/// generic "Access is denied" from downstream model calls does not reveal it.
/// </summary>
public static string LastUnlockStatus { get; private set; } = "NotAttempted";
public static bool TryUnlock()
{
// Only cache a successful unlock. Negative results (Unavailable, Unknown, exceptions)
// are often transient — e.g., AI feature stack not yet initialized after sign-in or
// sparse identity not fully applied to a freshly-started process — and retrying on
// the next call lets AP recover without restart.
if (_unlocked)
{
return true;
}
lock (_lock)
{
if (_unlocked)
{
return 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;
LastUnlockStatus = access.Status.ToString();
Debug.WriteLine($"Phi Silica LAF unlock status: {access.Status}");
}
catch (Exception ex)
{
LastUnlockStatus = "Exception: " + ex.Message;
Debug.WriteLine($"Phi Silica LAF unlock failed: {ex.Message}");
_unlocked = false;
}
return _unlocked;
}
}
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Threading;
using ManagedCommon;
@@ -14,16 +15,26 @@ namespace AdvancedPaste
public static class Program
{
[STAThread]
public static void Main(string[] args)
public static int Main(string[] args)
{
Logger.InitializeLogger("\\AdvancedPaste\\Logs");
WinRT.ComWrappersSupport.InitializeComWrappers();
if (args.Contains("--check-phi-silica", StringComparer.OrdinalIgnoreCase))
{
return CheckPhiSilicaAvailability();
}
if (args.Contains("--prepare-phi-silica", StringComparer.OrdinalIgnoreCase))
{
return PreparePhiSilica();
}
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 +52,100 @@ namespace AdvancedPaste
{
Logger.LogWarning("Another instance of AdvancedPasteUI is running. Exiting.");
}
return 0;
}
/// <summary>
/// Checks Phi Silica availability without starting the WinUI app.
/// Used by Settings UI to probe API status via subprocess.
/// Exit codes: 0 = available, 1 = not ready (model needs download), 2 = not supported or error.
/// </summary>
private static int CheckPhiSilicaAvailability()
{
try
{
PhiSilicaLafHelper.TryUnlock();
var readyState = Microsoft.Windows.AI.Text.LanguageModel.GetReadyState();
Console.Error.WriteLine($"[phi-silica] LAF unlock status: {PhiSilicaLafHelper.LastUnlockStatus}; ReadyState: {readyState}");
switch (readyState)
{
case Microsoft.Windows.AI.AIFeatureReadyState.Ready:
Console.Out.WriteLine("Available");
return 0;
case Microsoft.Windows.AI.AIFeatureReadyState.NotReady:
Console.Out.WriteLine("NotReady");
return 1;
default:
// NotSupportedOnCurrentSystem, DisabledByUser, CapabilityMissing,
// NotCompatibleWithSystemHardware, OSUpdateNeeded, or any future state:
// the model isn't usable and "Download model" (EnsureReadyAsync) won't fix it.
// CapabilityMissing in particular means the systemAIModels capability isn't
// authorized for the app, so EnsureReadyAsync throws E_ACCESSDENIED (0x80070005).
Console.Out.WriteLine("NotSupported");
return 2;
}
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
Console.Out.WriteLine("NotSupported");
return 2;
}
}
/// <summary>
/// Triggers Phi Silica model preparation (download) without starting the WinUI app.
/// Moves the model from NotReady to Ready by calling EnsureReadyAsync.
/// Exit codes: 0 = ready, 1 = preparation failed, 2 = not supported or error.
/// </summary>
private static int PreparePhiSilica()
{
try
{
PhiSilicaLafHelper.TryUnlock();
var readyState = Microsoft.Windows.AI.Text.LanguageModel.GetReadyState();
Console.Error.WriteLine($"[phi-silica] LAF unlock status: {PhiSilicaLafHelper.LastUnlockStatus}; ReadyState: {readyState}");
if (readyState is Microsoft.Windows.AI.AIFeatureReadyState.NotSupportedOnCurrentSystem
or Microsoft.Windows.AI.AIFeatureReadyState.DisabledByUser)
{
Console.Out.WriteLine("NotSupported");
return 2;
}
if (readyState == Microsoft.Windows.AI.AIFeatureReadyState.Ready)
{
Console.Out.WriteLine("Ready");
return 0;
}
// Run on a thread-pool (MTA) thread: the WinRT async operation does not
// marshal correctly when blocked on from the [STAThread] entry point.
var result = System.Threading.Tasks.Task.Run(
() => Microsoft.Windows.AI.Text.LanguageModel.EnsureReadyAsync().AsTask()).GetAwaiter().GetResult();
if (result.Status != Microsoft.Windows.AI.AIFeatureReadyResultState.Success)
{
int hresult = result.ExtendedError?.HResult ?? 0;
Console.Error.WriteLine($"[phi-silica] EnsureReadyAsync Status: {result.Status}; HRESULT: 0x{hresult:X8}; Message: {result.ExtendedError?.Message}");
Console.Error.WriteLine(result.ExtendedError?.Message ?? result.Status.ToString());
Console.Out.WriteLine("Failed");
return 1;
}
Console.Out.WriteLine("Ready");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
Console.Out.WriteLine("NotSupported");
return 2;
}
}
}
}

View File

@@ -33,14 +33,21 @@ public sealed class CustomActionKernelQueryCacheService : IKernelQueryCacheServi
private readonly IUserSettings _userSettings;
private readonly IFileSystem _fileSystem;
private readonly SettingsUtils _settingsUtil;
private readonly Func<string, string> _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<string, string> getLocalizedString)
{
_userSettings = userSettings;
_fileSystem = fileSystem;
_settingsUtil = new SettingsUtils(fileSystem);
_getLocalizedString = getLocalizedString;
_userSettings.Changed += OnUserSettingsChanged;
@@ -112,7 +119,7 @@ public sealed class CustomActionKernelQueryCacheService : IKernelQueryCacheServi
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;

View File

@@ -40,10 +40,15 @@ namespace AdvancedPaste.Services.CustomActions
this.userSettings = userSettings;
}
public async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress)
public async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> 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 @@ namespace AdvancedPaste.Services.CustomActions
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 @@ namespace AdvancedPaste.Services.CustomActions
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 @@ namespace AdvancedPaste.Services.CustomActions
{
AIServiceType.Onnx => false,
AIServiceType.Ollama => false,
AIServiceType.FoundryLocal => false,
AIServiceType.PhiSilica => false,
_ => true,
};
}

View File

@@ -12,6 +12,6 @@ namespace AdvancedPaste.Services.CustomActions
{
public interface ICustomActionTransformService
{
Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress);
Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress, string systemPromptOverride = null, string providerIdOverride = null);
}
}

View File

@@ -15,6 +15,7 @@ namespace AdvancedPaste.Services.CustomActions
SemanticKernelPasteProvider.Registration,
LocalModelPasteProvider.Registration,
FoundryLocalPasteProvider.Registration,
PhiSilicaPasteProvider.Registration,
};
private static readonly IReadOnlyDictionary<AIServiceType, Func<PasteAIConfig, IPasteAIProvider>> ProviderFactories = CreateProviderFactories();

View File

@@ -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<AIServiceType> 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<bool> IsAvailableAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
PhiSilicaLafHelper.TryUnlock();
var readyState = PhiSilicaLanguageModel.GetReadyState();
return Task.FromResult(readyState is not (AIFeatureReadyState.NotSupportedOnCurrentSystem or AIFeatureReadyState.DisabledByUser));
}
catch (Exception)
{
return Task.FromResult(false);
}
}
public async Task<string> ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress<double> 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<PhiSilicaLanguageModel> 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();
}
}
}

View File

@@ -175,6 +175,7 @@ namespace AdvancedPaste.Services.CustomActions
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = null,
ReasoningEffort = "minimal",
},
_ => new PromptExecutionSettings(),
};

View File

@@ -55,6 +55,13 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
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 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
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 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
case AIServiceType.ML:
case AIServiceType.Onnx:
case AIServiceType.Ollama:
case AIServiceType.PhiSilica:
return null;
default:
return null;

View File

@@ -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;
/// <summary>
@@ -21,6 +23,14 @@ public interface IAICredentialsProvider
/// <returns>Credential string or <see cref="string.Empty"/> when missing.</returns>
string GetKey();
/// <summary>
/// Retrieves the credential for a specific AI provider.
/// </summary>
/// <param name="serviceType">The AI service type.</param>
/// <param name="providerId">The provider identifier.</param>
/// <returns>Credential string or <see cref="string.Empty"/> when missing.</returns>
string GetKey(AIServiceType serviceType, string providerId);
/// <summary>
/// Refreshes the cached credential for the active AI provider.
/// </summary>

View File

@@ -12,5 +12,5 @@ namespace AdvancedPaste.Services;
public interface IKernelService
{
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress);
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress, string providerIdOverride = null);
}

View File

@@ -46,7 +46,7 @@ public abstract class KernelServiceBase(
protected abstract IKernelRuntimeConfiguration GetRuntimeConfiguration();
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress)
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress, string providerIdOverride = null)
{
Logger.LogTrace();

View File

@@ -9,15 +9,18 @@ using System.Threading.Tasks;
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<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress)
{
@@ -36,8 +39,9 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct
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 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct
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;
}
}

View File

@@ -232,6 +232,12 @@
<data name="PasteAsPlainText" xml:space="preserve">
<value>Paste as plain text</value>
</data>
<data name="FixSpellingAndGrammar" xml:space="preserve">
<value>Fix spelling and grammar</value>
</data>
<data name="CoachingExplanationTitle.Text" xml:space="preserve">
<value>What was changed and why</value>
</data>
<data name="ImageToText" xml:space="preserve">
<value>Image to text</value>
</data>

View File

@@ -16,8 +16,8 @@ using System.Threading.Tasks;
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 @@ namespace AdvancedPaste.ViewModels
private readonly IUserSettings _userSettings;
private readonly IPasteFormatExecutor _pasteFormatExecutor;
private readonly IAICredentialsProvider _credentialsProvider;
private readonly ICustomActionTransformService _customActionTransformService;
private CancellationTokenSource _pasteActionCancellationTokenSource;
@@ -258,11 +259,12 @@ namespace AdvancedPaste.ViewModels
public event EventHandler PreviewRequested;
public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider credentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider credentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor, ICustomActionTransformService customActionTransformService)
{
_credentialsProvider = credentialsProvider;
_userSettings = userSettings;
_pasteFormatExecutor = pasteFormatExecutor;
_customActionTransformService = customActionTransformService;
GeneratedResponses = [];
GeneratedResponses.CollectionChanged += (s, e) =>
@@ -341,11 +343,21 @@ namespace AdvancedPaste.ViewModels
});
}
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 @@ namespace AdvancedPaste.ViewModels
UpdateFormats(
CustomActionPasteFormats,
IsCustomAIServiceEnabled ? _userSettings.CustomActions.Select(customAction => 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 @@ namespace AdvancedPaste.ViewModels
{
PasteActionError = PasteActionError.None;
Query = string.Empty;
CoachingExplanation = null;
await ReadClipboardAsync();
@@ -616,6 +629,12 @@ namespace AdvancedPaste.ViewModels
[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 @@ namespace AdvancedPaste.ViewModels
[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 @@ namespace AdvancedPaste.ViewModels
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 @@ namespace AdvancedPaste.ViewModels
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)

View File

@@ -1,40 +1,6 @@
#include <windows.h>
#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

View File

@@ -15,7 +15,6 @@
</PropertyGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">

View File

@@ -100,25 +100,30 @@ 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);
const auto launch_direct_exe = [&]() -> HRESULT {
// 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\\PowerToys.AdvancedPaste.exe";
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;
return S_OK;
}
else
{
Logger::error(L"Advanced Paste process failed to start. {}", get_last_error_or_default(GetLastError()));
return E_FAIL;
}
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
sei.lpFile = L"WinUI3Apps\\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;
}
};
return launch_direct_exe();
}
HRESULT AdvancedPasteProcessManager::start_named_pipe_server(const std::wstring& pipe_name)
@@ -175,8 +180,9 @@ HRESULT AdvancedPasteProcessManager::start_named_pipe_server(const std::wstring&
}
}
// Wait for client.
const constexpr DWORD client_timeout_millis = 5000;
// Wait for client. AdvancedPaste under sparse identity can take >5s on cold start to
// bootstrap WinAppSDK + DI host before connecting back to this pipe.
const constexpr DWORD client_timeout_millis = 15000;
switch (WaitForSingleObject(overlapped.hEvent, client_timeout_millis))
{
case WAIT_OBJECT_0:

View File

@@ -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 @@ private:
};
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 @@ private:
// Define the expected order to ensure consistent hotkey ID assignment
const std::vector<winrt::hstring> expectedOrder = {
L"image-to-text",
L"fix-spelling-and-grammar",
L"paste-as-file",
L"transcode"
};
@@ -982,6 +1000,12 @@ public:
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);
@@ -1032,13 +1056,11 @@ public:
}
}
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())
{
Logger::warn(L"Auto-copy: failed to copy selection for custom action index {} — aborting action", custom_action_index);
return false;
}
send_copy_selection(); // best-effort; ignore failure
}
m_process_manager.start();

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This file is read by XES, which we use in our Release builds. -->
<PropertyGroup Label="Version">
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
<VersionMajor>0</VersionMajor>
<VersionMinor>9</VersionMinor>
<VersionInfoProductName>PowerToys Advanced Paste</VersionInfoProductName>
</PropertyGroup>
</Project>

View File

@@ -19,5 +19,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Google,
AzureAIInference,
Ollama,
PhiSilica,
}
}

View File

@@ -31,6 +31,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
"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 @@ namespace Microsoft.PowerToys.Settings.UI.Library
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 @@ namespace Microsoft.PowerToys.Settings.UI.Library
AIServiceType.Google => "google",
AIServiceType.AzureAIInference => "azureaiinference",
AIServiceType.Ollama => "ollama",
AIServiceType.PhiSilica => "phisilica",
_ => string.Empty,
};
}

View File

@@ -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,

View File

@@ -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 sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
}
}
[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 sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
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
{

View File

@@ -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 sealed class AdvancedPasteAdditionalActions
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 sealed class AdvancedPasteAdditionalActions
public IEnumerable<IAdvancedPasteAction> GetAllActions()
{
return GetAllActionsRecursive([ImageToText, PasteAsFile, Transcode]);
return GetAllActionsRecursive([ImageToText, FixSpellingAndGrammar, PasteAsFile, Transcode]);
}
/// <summary>

View File

@@ -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 sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
}
}
[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 sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
Name = other.Name;
Description = other.Description;
Prompt = other.Prompt;
ProviderId = other.ProviderId;
Shortcut = other.GetShortcutClone();
IsShown = other.IsShown;
CanMoveUp = other.CanMoveUp;

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
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.";
}

View File

@@ -68,6 +68,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
string[] additionalActionHeaderKeys =
[
"ImageToText",
"FixSpellingAndGrammar",
"PasteAsTxtFile",
"PasteAsPngFile",
"PasteAsHtmlFile",
@@ -79,11 +80,25 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
if (action is AdvancedPasteAdditionalAction additionalAction)
{
var headerKey = additionalActionHeaderKeys[Math.Min(index, additionalActionHeaderKeys.Length - 1)];
hotkeyAccessors.Add(new HotkeyAccessor(
() => additionalAction.Shortcut,
value => additionalAction.Shortcut = value ?? new HotkeySettings(),
additionalActionHeaderKeys[index]));
headerKey));
index++;
// The coaching shortcut is registered by the runner as a separate hotkey
// immediately after Fix Spelling and Grammar (and only when it's active), so it
// must appear in the same position here to keep hotkey IDs aligned with conflicts.
if (ReferenceEquals(additionalAction, Properties.AdditionalActions.FixSpellingAndGrammar)
&& additionalAction.CoachingEnabled
&& additionalAction.CoachingShortcut is { Code: not 0 })
{
hotkeyAccessors.Add(new HotkeyAccessor(
() => additionalAction.CoachingShortcut,
value => additionalAction.CoachingShortcut = value ?? new HotkeySettings(),
"FixSpellingAndGrammarCoaching"));
}
}
}

View File

@@ -116,6 +116,17 @@
Header="{x:Bind ModelName, Mode=OneWay}"
HeaderIcon="{x:Bind ServiceType, Mode=OneWay, Converter={StaticResource ServiceTypeToIconConverter}}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Border
Padding="6,2"
VerticalAlignment="Center"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="4"
Visibility="{x:Bind IsActive, Mode=OneWay}">
<TextBlock
x:Uid="AdvancedPaste_DefaultBadge"
FontSize="12"
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
</Border>
<Button
Padding="8"
Background="Transparent"
@@ -124,6 +135,11 @@
<FontIcon FontSize="16" Glyph="&#xE712;" />
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="AdvancedPaste_SetAsDefault"
Click="SetAsDefaultProviderButton_Click"
Icon="{ui:FontIcon Glyph=&#xE735;}"
Tag="{x:Bind}" />
<MenuFlyoutItem
x:Uid="AdvancedPaste_Edit"
Click="EditPasteAIProviderButton_Click"
@@ -164,7 +180,7 @@
</InfoBar.IconSource>
</InfoBar>
</tkcontrols:SettingsExpander.ItemsHeader>
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.AdvancedPasteUIShortcut, Mode=TwoWay}" />
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.AdvancedPasteUIShortcut, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard Name="AdvancedPasteEnableClipboardPreview" ContentAlignment="Left">
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="AdvancedPaste_EnableClipboardPreview" IsChecked="{x:Bind ViewModel.EnableClipboardPreview, Mode=TwoWay}" />
@@ -195,7 +211,7 @@
Name="PasteAsPlainTextShortcut"
x:Uid="PasteAsPlainText_Shortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xE8E9;}">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.PasteAsPlainTextShortcut, Mode=TwoWay}" />
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.PasteAsPlainTextShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="PasteAsMarkdownShortcut"
@@ -222,6 +238,109 @@
HeaderIcon="{ui:FontIcon Glyph=&#xE91B;}">
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsExpander
x:Name="FixSpellingAndGrammar"
x:Uid="FixSpellingAndGrammar"
DataContext="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar, Mode=OneWay}"
HeaderIcon="{ui:FontIcon Glyph=&#xE8E2;}"
IsEnabled="{x:Bind ViewModel.IsAIEnabled, Mode=OneWay}"
IsExpanded="{Binding IsShown, Mode=OneWay}">
<tkcontrols:SettingsExpander.Content>
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
</tkcontrols:SettingsExpander.Content>
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard Visibility="Collapsed" />
<tkcontrols:SettingsCard x:Uid="AdvancedPaste_ActionProvider" IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.IsShown, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="4">
<ComboBox
x:Name="FixSpellingProviderComboBox"
MinWidth="200"
DisplayMemberPath="DisplayName"
ItemsSource="{x:Bind ViewModel.PasteAIConfiguration.Providers, Mode=OneWay}"
PlaceholderText="{x:Bind GetDefaultProviderLabel()}"
SelectedValue="{Binding ProviderId, Mode=TwoWay}"
SelectedValuePath="Id" />
<Button
x:Uid="AdvancedPaste_ClearProviderSelection"
VerticalAlignment="Center"
Click="ClearProviderSelection_Click"
Content="{ui:FontIcon Glyph=&#xE894;,
FontSize=12}"
Style="{StaticResource SubtleButtonStyle}"
Tag="{x:Bind FixSpellingProviderComboBox}" />
</StackPanel>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="FixSpellingAndGrammar_CustomPrompt"
HorizontalContentAlignment="Stretch"
ContentAlignment="Vertical"
IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.IsShown, Mode=OneWay}">
<TextBox PlaceholderText="{x:Bind models:AdvancedPasteDefaultPrompts.FixSpellingAndGrammar}" Text="{Binding Prompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="FixSpellingAndGrammar_SystemPrompt"
HorizontalContentAlignment="Stretch"
ContentAlignment="Vertical"
IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.IsShown, Mode=OneWay}">
<TextBox PlaceholderText="{x:Bind models:AdvancedPasteDefaultPrompts.FixSpellingAndGrammarSystem}" Text="{Binding SystemPrompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</tkcontrols:SettingsCard>
<!-- Coaching mode -->
<tkcontrols:SettingsCard x:Uid="FixSpellingAndGrammar_CoachingSection" IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.IsShown, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="4">
<controls:ShortcutControl
MinWidth="{StaticResource SettingActionControlMinWidth}"
AllowDisable="True"
HotkeySettings="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingShortcut, Mode=TwoWay}" />
<ToggleSwitch
IsOn="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=TwoWay}"
OffContent=""
OnContent="" />
</StackPanel>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="FixSpellingAndGrammar_CoachingProvider"
IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}"
Visibility="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="4">
<ComboBox
x:Name="CoachingProviderComboBox"
MinWidth="200"
DisplayMemberPath="DisplayName"
ItemsSource="{x:Bind ViewModel.PasteAIConfiguration.Providers, Mode=OneWay}"
PlaceholderText="{x:Bind GetDefaultProviderLabel()}"
SelectedValue="{Binding CoachingProviderId, Mode=TwoWay}"
SelectedValuePath="Id" />
<Button
x:Uid="AdvancedPaste_ClearProviderSelection"
VerticalAlignment="Center"
Click="ClearProviderSelection_Click"
Content="{ui:FontIcon Glyph=&#xE894;,
FontSize=12}"
Style="{StaticResource SubtleButtonStyle}"
Tag="{x:Bind CoachingProviderComboBox}" />
</StackPanel>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="FixSpellingAndGrammar_CoachingPrompt"
HorizontalContentAlignment="Stretch"
ContentAlignment="Vertical"
IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}"
Visibility="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}">
<TextBox PlaceholderText="{x:Bind models:AdvancedPasteDefaultPrompts.FixSpellingAndGrammarCoaching}" Text="{Binding CoachingPrompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="FixSpellingAndGrammar_CoachingSystemPrompt"
HorizontalContentAlignment="Stretch"
ContentAlignment="Vertical"
IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}"
Visibility="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}">
<TextBox PlaceholderText="{x:Bind models:AdvancedPasteDefaultPrompts.FixSpellingAndGrammarCoachingSystem}" Text="{Binding CoachingSystemPrompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Visibility="Collapsed" />
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
<tkcontrols:SettingsExpander
Name="PasteAsFile"
x:Uid="PasteAsFile"
@@ -306,7 +425,7 @@
<!-- Custom actions -->
<controls:SettingsGroup x:Uid="AdvancedPaste_Additional_Actions_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander
Name="AdvancedPasteUIActions"
x:Name="AdvancedPasteUIActions"
x:Uid="AdvancedPasteUI_Actions"
HeaderIcon="{ui:FontIcon Glyph=&#xE792;}"
IsEnabled="{x:Bind ViewModel.IsAIEnabled, Mode=OneWay}"
@@ -425,6 +544,28 @@
AcceptsReturn="true"
Text="{Binding Prompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextWrapping="Wrap" />
<StackPanel
HorizontalAlignment="Left"
Orientation="Horizontal"
Spacing="4">
<ComboBox
x:Name="CustomActionProviderComboBox"
x:Uid="AdvancedPaste_CustomAction_Provider"
Width="306"
DisplayMemberPath="DisplayName"
ItemsSource="{x:Bind ViewModel.PasteAIConfiguration.Providers, Mode=OneWay}"
PlaceholderText="{x:Bind GetDefaultProviderLabel()}"
SelectedValue="{Binding ProviderId, Mode=TwoWay}"
SelectedValuePath="Id" />
<Button
x:Uid="AdvancedPaste_ClearProviderSelection"
VerticalAlignment="Bottom"
Click="ClearProviderSelection_Click"
Content="{ui:FontIcon Glyph=&#xE894;,
FontSize=12}"
Style="{StaticResource SubtleButtonStyle}"
Tag="{x:Bind CustomActionProviderComboBox}" />
</StackPanel>
</StackPanel>
</ContentDialog>
@@ -542,6 +683,92 @@
<controls:FoundryLocalModelPicker x:Name="FoundryLocalPicker" />
</Grid>
<Grid
x:Name="PhiSilicaPanel"
Margin="0,8,0,0"
Visibility="Collapsed">
<StackPanel
x:Name="PhiSilicaLoadingPanel"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<ProgressRing
Width="36"
Height="36"
HorizontalAlignment="Center" />
<TextBlock
x:Name="PhiSilicaLoadingText"
x:Uid="AdvancedPaste_PhiSilicaLoadingStatus"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel
x:Name="PhiSilicaAvailablePanel"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8"
Visibility="Collapsed">
<FontIcon
HorizontalAlignment="Center"
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="24"
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
Glyph="&#xE73E;" />
<TextBlock
x:Name="PhiSilicaAvailableText"
HorizontalAlignment="Center"
FontWeight="SemiBold"
TextAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel
x:Name="PhiSilicaNotAvailablePanel"
Margin="48,0,48,48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8"
Visibility="Collapsed">
<Image Width="36" Source="ms-appx:///Assets/Settings/Icons/Models/WindowsML.svg" />
<TextBlock
x:Name="PhiSilicaNotAvailableTitle"
HorizontalAlignment="Center"
FontWeight="SemiBold"
TextAlignment="Center"
TextWrapping="Wrap" />
<TextBlock
x:Name="PhiSilicaNotAvailableDescription"
HorizontalAlignment="Center"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextAlignment="Center"
TextWrapping="Wrap" />
<TextBlock
x:Name="PhiSilicaNotAvailableDetails"
HorizontalAlignment="Center"
FontFamily="Consolas"
FontSize="11"
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
IsTextSelectionEnabled="True"
TextAlignment="Center"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Button
x:Name="PhiSilicaPrepareButton"
x:Uid="AdvancedPaste_PhiSilicaPrepareButton"
Margin="0,4,0,0"
HorizontalAlignment="Center"
Click="PhiSilicaPrepareButton_Click"
Style="{StaticResource AccentButtonStyle}"
Visibility="Collapsed" />
<HyperlinkButton
x:Uid="AdvancedPaste_PhiSilicaCopilotLearnMore"
HorizontalAlignment="Center"
NavigateUri="https://learn.microsoft.com/windows/ai/npu-devices/" />
</StackPanel>
</Grid>
<StackPanel
x:Name="PasteAIModelPanel"
Orientation="Horizontal"

View File

@@ -6,6 +6,8 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
@@ -30,6 +32,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private CancellationTokenSource _foundryModelLoadCts;
private bool _suppressFoundrySelectionChanged;
private bool _isFoundryLocalAvailable;
private bool _isPhiSilicaAvailable;
private bool _disposed;
private const string PasteAiDialogDefaultTitle = "Paste with AI provider configuration";
@@ -37,6 +40,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private const string SimpleAISystemPrompt = "You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it. Do not output anything else besides the reformatted clipboard content.";
private static readonly string AdvancedAISystemPromptNormalized = AdvancedAISystemPrompt.Trim();
private static readonly string SimpleAISystemPromptNormalized = SimpleAISystemPrompt.Trim();
private static readonly char[] NewLineSeparators = ['\r', '\n'];
private AdvancedPasteViewModel ViewModel { get; set; }
@@ -65,6 +69,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ViewModel.OnPageLoaded();
UpdatePasteAIUIVisibility();
await UpdateFoundryLocalUIAsync();
await UpdatePhiSilicaUIAsync();
};
Unloaded += (_, _) =>
@@ -83,6 +88,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ViewModel.RefreshEnabledState();
UpdatePasteAIUIVisibility();
_ = UpdateFoundryLocalUIAsync();
_ = UpdatePhiSilicaUIAsync();
}
private void EnableAdvancedPasteAI() => ViewModel.EnableAI();
@@ -103,6 +109,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
else
{
ViewModel.DisableAI();
FixSpellingAndGrammar.IsExpanded = false;
AdvancedPasteUIActions.IsExpanded = false;
}
}
@@ -319,7 +327,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
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 +354,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
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 +383,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
// 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 +436,360 @@ namespace Microsoft.PowerToys.Settings.UI.Views
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();
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
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 (status, diagnostics) = await Task.Run(() => CheckPhiSilicaViaAdvancedPaste());
if (status == "NotSupported")
{
_isPhiSilicaAvailable = false;
ShowPhiSilicaNotAvailableState(
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotAvailable_Title"),
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotAvailable_Description"),
details: diagnostics);
}
else if (status == "NotReady")
{
_isPhiSilicaAvailable = false;
ShowPhiSilicaNotAvailableState(
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotReady_Title"),
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotReady_Description"),
showPrepareButton: true,
details: diagnostics);
}
else
{
_isPhiSilicaAvailable = true;
ShowPhiSilicaAvailableState(resourceLoader.GetString("AdvancedPaste_PhiSilicaAvailable_Message"));
}
}
catch (Exception)
{
_isPhiSilicaAvailable = false;
ShowPhiSilicaNotAvailableState(
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotAvailable_Title"),
resourceLoader.GetString("AdvancedPaste_PhiSilicaCheckFailed_Description"));
}
if (PasteAIProviderConfigurationDialog is not null)
{
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = _isPhiSilicaAvailable;
}
}
private void ShowPhiSilicaLoadingState(string message = null)
{
if (PhiSilicaLoadingText is not null && !string.IsNullOrEmpty(message))
{
PhiSilicaLoadingText.Text = message;
}
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, bool showPrepareButton = false, string details = null)
{
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;
}
if (PhiSilicaPrepareButton is not null)
{
PhiSilicaPrepareButton.Visibility = showPrepareButton ? Visibility.Visible : Visibility.Collapsed;
}
// Surface the AdvancedPaste diagnostics (LAF status, ready state, HRESULT) so the user
// can see why the model isn't ready / why a download attempt failed, instead of nothing.
if (PhiSilicaNotAvailableDetails is not null)
{
PhiSilicaNotAvailableDetails.Text = details ?? string.Empty;
PhiSilicaNotAvailableDetails.Visibility = string.IsNullOrWhiteSpace(details) ? Visibility.Collapsed : Visibility.Visible;
}
}
/// <summary>
/// Checks Phi Silica availability by launching AdvancedPaste.exe with --check-phi-silica.
/// AdvancedPaste has sparse package identity and can call the Windows AI APIs directly.
/// Returns the status ("Available", "NotReady", or "NotSupported") plus any diagnostic lines
/// AdvancedPaste wrote to stderr (LAF unlock status, ready state).
/// </summary>
private static (string Status, string Diagnostics) CheckPhiSilicaViaAdvancedPaste()
{
var settingsDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
// PowerToys.AdvancedPaste.exe ships in the same WinUI3Apps folder as PowerToys.Settings.exe
// (see installer harvest and .vscode/launch.json), not in an "AdvancedPaste" subfolder.
var advancedPastePath = Path.Combine(settingsDir, "PowerToys.AdvancedPaste.exe");
if (!File.Exists(advancedPastePath))
{
return ("NotSupported", string.Empty);
}
var startInfo = new ProcessStartInfo
{
FileName = advancedPastePath,
Arguments = "--check-phi-silica",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
using var process = Process.Start(startInfo);
if (process == null)
{
return ("NotSupported", string.Empty);
}
// Read stdout/stderr asynchronously so a stalled child can't block us before the timeout elapses.
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
if (!process.WaitForExit(10_000))
{
TryKillProcess(process);
return ("NotSupported", string.Empty);
}
var output = outputTask.GetAwaiter().GetResult().Trim();
var diagnostics = ExtractPhiSilicaDiagnostics(errorTask.GetAwaiter().GetResult());
var status = output switch
{
"Available" => "Available",
"NotReady" => "NotReady",
_ => "NotSupported",
};
return (status, diagnostics);
}
/// <summary>
/// Triggers Phi Silica model preparation (download) by launching AdvancedPaste.exe with
/// --prepare-phi-silica. AdvancedPaste has sparse package identity and can call EnsureReadyAsync.
/// Returns the status ("Ready", "Failed", or "NotSupported") plus any diagnostic lines
/// AdvancedPaste wrote to stderr (ready state and, on failure, the EnsureReadyAsync HRESULT).
/// </summary>
private static (string Status, string Diagnostics) PreparePhiSilicaViaAdvancedPaste()
{
var settingsDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
var advancedPastePath = Path.Combine(settingsDir, "PowerToys.AdvancedPaste.exe");
if (!File.Exists(advancedPastePath))
{
return ("NotSupported", string.Empty);
}
var startInfo = new ProcessStartInfo
{
FileName = advancedPastePath,
Arguments = "--prepare-phi-silica",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
using var process = Process.Start(startInfo);
if (process == null)
{
return ("NotSupported", string.Empty);
}
// Read stdout/stderr asynchronously; model download can take a while, but a stalled child
// must not block us indefinitely, so cap the wait and kill the process on timeout.
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
if (!process.WaitForExit(600_000))
{
TryKillProcess(process);
return ("Failed", string.Empty);
}
var output = outputTask.GetAwaiter().GetResult().Trim();
var diagnostics = ExtractPhiSilicaDiagnostics(errorTask.GetAwaiter().GetResult());
var status = output switch
{
"Ready" => "Ready",
"Failed" => "Failed",
_ => "NotSupported",
};
return (status, diagnostics);
}
private static void TryKillProcess(Process process)
{
try
{
process.Kill(entireProcessTree: true);
}
catch (Exception)
{
}
}
// AdvancedPaste writes structured Phi Silica diagnostics to stderr, one per line prefixed with
// "[phi-silica] " (LAF unlock status, ready state, and on failure the EnsureReadyAsync HRESULT
// and message). Pull those lines out so the configurator can show the real error/HRESULT.
private static string ExtractPhiSilicaDiagnostics(string standardError)
{
if (string.IsNullOrWhiteSpace(standardError))
{
return string.Empty;
}
const string prefix = "[phi-silica] ";
var lines = standardError
.Split(NewLineSeparators, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
.Where(line => line.StartsWith(prefix, StringComparison.Ordinal))
.Select(line => line.Substring(prefix.Length))
.Distinct()
.ToArray();
return string.Join(Environment.NewLine, lines);
}
private async void PhiSilicaPrepareButton_Click(object sender, RoutedEventArgs e)
{
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
ShowPhiSilicaLoadingState(resourceLoader.GetString("AdvancedPaste_PhiSilicaPreparing_Status"));
if (PasteAIProviderConfigurationDialog is not null)
{
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
}
(string Status, string Diagnostics) prepareResult;
try
{
prepareResult = await Task.Run(() => PreparePhiSilicaViaAdvancedPaste());
}
catch (Exception ex)
{
prepareResult = ("Failed", ex.Message);
}
if (prepareResult.Status == "Ready")
{
// Model is now ready; re-probe so the UI flips to the available state and Save enables.
await UpdatePhiSilicaUIAsync();
return;
}
_isPhiSilicaAvailable = false;
if (prepareResult.Status == "NotSupported")
{
ShowPhiSilicaNotAvailableState(
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotAvailable_Title"),
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotAvailable_Description"),
details: prepareResult.Diagnostics);
}
else
{
ShowPhiSilicaNotAvailableState(
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotReady_Title"),
resourceLoader.GetString("AdvancedPaste_PhiSilicaPrepareFailed_Description"),
showPrepareButton: true,
details: prepareResult.Diagnostics);
}
if (PasteAIProviderConfigurationDialog is not null)
{
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
}
}
private async Task LoadFoundryLocalModelsAsync()
{
if (FoundryLocalPanel is null)
@@ -835,6 +1204,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
AIServiceType.Onnx => false,
AIServiceType.Ollama => false,
AIServiceType.FoundryLocal => false,
AIServiceType.PhiSilica => false,
AIServiceType.ML => false,
_ => true,
};
@@ -1112,6 +1482,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
await UpdateFoundryLocalUIAsync();
await UpdatePhiSilicaUIAsync();
UpdatePasteAIUIVisibility();
RefreshDialogBindings();
@@ -1141,6 +1512,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
UpdatePasteAIUIVisibility();
await UpdateFoundryLocalUIAsync();
await UpdatePhiSilicaUIAsync();
RefreshDialogBindings();
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(provider.Id, provider.ServiceType);
await PasteAIProviderConfigurationDialog.ShowAsync();
@@ -1157,11 +1529,41 @@ namespace Microsoft.PowerToys.Settings.UI.Views
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)";
}
}
}
}

View File

@@ -1992,6 +1992,86 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
<data name="ImageToText.Header" xml:space="preserve">
<value>Image to text</value>
</data>
<data name="FixSpellingAndGrammar.Header" xml:space="preserve">
<value>Fix spelling and grammar</value>
</data>
<data name="FixSpellingAndGrammar_CustomPrompt.Header" xml:space="preserve">
<value>Custom prompt</value>
</data>
<data name="FixSpellingAndGrammar_CustomPrompt.Description" xml:space="preserve">
<value>Override the default AI prompt used for fixing spelling and grammar. Leave empty to use the default prompt.</value>
</data>
<data name="FixSpellingAndGrammar_SystemPrompt.Header" xml:space="preserve">
<value>System prompt</value>
<comment>Header for the system prompt customization for fix spelling action</comment>
</data>
<data name="FixSpellingAndGrammar_SystemPrompt.Description" xml:space="preserve">
<value>Override the system prompt that sets the AI's role for fixing spelling and grammar. Leave empty to use the default.</value>
<comment>Description for the system prompt customization</comment>
</data>
<data name="AdvancedPaste_ActionProvider.Header" xml:space="preserve">
<value>AI model</value>
<comment>Label for selecting which AI model provider to use for this action</comment>
</data>
<data name="AdvancedPaste_ActionProvider.Description" xml:space="preserve">
<value>Select the AI model to use for this action. Leave as default to use the globally active provider.</value>
</data>
<data name="AdvancedPaste_ActionProvider_Default" xml:space="preserve">
<value>Default (use active provider)</value>
<comment>Option in provider picker that means use the globally configured provider</comment>
</data>
<data name="AdvancedPaste_CustomAction_Provider.Header" xml:space="preserve">
<value>AI model</value>
<comment>Label for selecting the AI model provider in the custom action dialog</comment>
</data>
<data name="AdvancedPaste_ClearProviderSelection.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Reset to default provider</value>
<comment>Tooltip for button that clears the provider selection back to default</comment>
</data>
<data name="AdvancedPaste_ClearProviderSelection.AutomationProperties.Name" xml:space="preserve">
<value>Reset to default provider</value>
<comment>Accessible name for button that clears the provider selection back to default</comment>
</data>
<data name="FixSpellingAndGrammar_CoachingSection.Header" xml:space="preserve">
<value>Coaching mode</value>
<comment>Header for the coaching mode collapsible settings section</comment>
</data>
<data name="FixSpellingAndGrammar_CoachingSection.Description" xml:space="preserve">
<value>When enabled, shows a preview with an explanation of what was fixed and why.</value>
<comment>Description for the coaching mode settings section</comment>
</data>
<data name="FixSpellingAndGrammar_CoachingShortcut.Header" xml:space="preserve">
<value>Coaching shortcut</value>
<comment>Header for the coaching mode keyboard shortcut</comment>
</data>
<data name="FixSpellingAndGrammar_CoachingShortcut.Description" xml:space="preserve">
<value>Optional shortcut that triggers fix spelling with coaching enabled. If not set, uses the main action shortcut.</value>
<comment>Description for the coaching shortcut setting</comment>
</data>
<data name="FixSpellingAndGrammar_CoachingPrompt.Header" xml:space="preserve">
<value>Coaching prompt</value>
<comment>Header for the coaching prompt customization text box</comment>
</data>
<data name="FixSpellingAndGrammar_CoachingProvider.Header" xml:space="preserve">
<value>Coaching AI model</value>
<comment>Label for selecting which AI model provider to use for coaching explanations</comment>
</data>
<data name="FixSpellingAndGrammar_CoachingProvider.Description" xml:space="preserve">
<value>Select the AI model to use for coaching explanations. Leave as default to use the same model as Fix Spelling.</value>
<comment>Description for the coaching provider selector</comment>
</data>
<data name="FixSpellingAndGrammar_CoachingPrompt.Description" xml:space="preserve">
<value>Override the AI prompt used when generating the coaching explanation. Leave empty to use the default prompt.</value>
<comment>Description for the coaching prompt customization</comment>
</data>
<data name="FixSpellingAndGrammar_CoachingSystemPrompt.Header" xml:space="preserve">
<value>Coaching system prompt</value>
<comment>Header for the coaching system prompt customization text box</comment>
</data>
<data name="FixSpellingAndGrammar_CoachingSystemPrompt.Description" xml:space="preserve">
<value>Override the system prompt that sets the AI's role for coaching explanations. Leave empty to use the default.</value>
<comment>Description for the coaching system prompt customization</comment>
</data>
<data name="PasteAsFile.Header" xml:space="preserve">
<value>Paste as file</value>
</data>
@@ -4391,11 +4471,11 @@ Activate by holding the key for the character you want to add an accent to, then
<comment>Enables display of clipboard contents preview in the Advanced Paste window</comment>
</data>
<data name="AdvancedPaste_AutoCopySelectionForCustomActionHotkey.Header" xml:space="preserve">
<value>Auto-copy selection for custom action hotkeys</value>
<value>Use selected text for all hotkeys</value>
<comment>Advanced Paste is a product name</comment>
</data>
<data name="AdvancedPaste_AutoCopySelectionForCustomActionHotkey.Description" xml:space="preserve">
<value>Attempts to copy the current selection before running a custom action shortcut</value>
<value>Attempts to copy the current text selection before running any Advanced Paste shortcut. Falls back to clipboard contents if nothing is selected.</value>
<comment>Advanced Paste is a product name</comment>
</data>
<data name="GPO_CommandNotFound_ForceDisabled.Title" xml:space="preserve">
@@ -5963,6 +6043,14 @@ The break timer font matches the text font.</value>
<data name="AdvancedPaste_Edit.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="AdvancedPaste_SetAsDefault.Text" xml:space="preserve">
<value>Set as default</value>
<comment>Menu item to mark a provider as the default/active one</comment>
</data>
<data name="AdvancedPaste_DefaultBadge.Text" xml:space="preserve">
<value>Default</value>
<comment>Badge label shown on the provider that is currently the default/active one</comment>
</data>
<data name="AdvancedPaste_Remove.Text" xml:space="preserve">
<value>Remove</value>
</data>
@@ -6003,6 +6091,46 @@ The break timer font matches the text font.</value>
<value>Foundry Local is still in public preview</value>
<comment>Do not loc "Foundry Local"</comment>
</data>
<data name="AdvancedPaste_PhiSilicaLoadingStatus.Text" xml:space="preserve">
<value>Checking Phi Silica availability...</value>
<comment>Do not localize "Phi Silica", it's a model name</comment>
</data>
<data name="AdvancedPaste_PhiSilicaCopilotLearnMore.Content" xml:space="preserve">
<value>Learn more about Copilot+ PCs</value>
<comment>Do not localize "Copilot+ PCs", it's a product name</comment>
</data>
<data name="AdvancedPaste_PhiSilicaNotAvailable_Title" xml:space="preserve">
<value>Phi Silica is not available on this device.</value>
<comment>Do not localize "Phi Silica", it's a model name</comment>
</data>
<data name="AdvancedPaste_PhiSilicaNotAvailable_Description" xml:space="preserve">
<value>A Copilot+ PC with an NPU is required to use Phi Silica. For on-device AI on any Windows PC, consider using Foundry Local.</value>
<comment>Do not localize "Copilot+ PC", "NPU", "Phi Silica", and "Foundry Local", they are product/technical names</comment>
</data>
<data name="AdvancedPaste_PhiSilicaNotReady_Title" xml:space="preserve">
<value>Phi Silica model is not ready.</value>
<comment>Do not localize "Phi Silica", it's a model name</comment>
</data>
<data name="AdvancedPaste_PhiSilicaNotReady_Description" xml:space="preserve">
<value>The model needs to be downloaded before it can be used. Select "Download model" to start, or check Windows Update for progress.</value>
</data>
<data name="AdvancedPaste_PhiSilicaPrepareButton.Content" xml:space="preserve">
<value>Download model</value>
</data>
<data name="AdvancedPaste_PhiSilicaPreparing_Status" xml:space="preserve">
<value>Downloading and preparing the model. This may take several minutes...</value>
</data>
<data name="AdvancedPaste_PhiSilicaPrepareFailed_Description" xml:space="preserve">
<value>Couldn't prepare the model. Check Windows Update for the AI model download, then try again.</value>
</data>
<data name="AdvancedPaste_PhiSilicaAvailable_Message" xml:space="preserve">
<value>Phi Silica is available and ready on this device.</value>
<comment>Do not localize "Phi Silica", it's a model name</comment>
</data>
<data name="AdvancedPaste_PhiSilicaCheckFailed_Description" xml:space="preserve">
<value>Unable to check Phi Silica availability. A Copilot+ PC with an NPU is required.</value>
<comment>Do not localize "Phi Silica", "Copilot+ PC", and "NPU", they are product/technical names</comment>
</data>
<data name="CmdPal_Settings.Description" xml:space="preserve">
<value>Configure the activation shortcut, extensions, behavior and much more</value>
</data>

View File

@@ -139,6 +139,15 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (action is AdvancedPasteAdditionalAction additionalAction)
{
hotkeySettings.Add(additionalAction.Shortcut);
// Mirror the runner's hotkey order: the coaching shortcut is registered as a
// separate hotkey immediately after Fix Spelling and Grammar when it's active.
if (ReferenceEquals(additionalAction, _additionalActions.FixSpellingAndGrammar)
&& additionalAction.CoachingEnabled
&& additionalAction.CoachingShortcut is { Code: not 0 })
{
hotkeySettings.Add(additionalAction.CoachingShortcut);
}
}
}
@@ -492,6 +501,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
var newValue = value ?? new PasteAIConfiguration();
_advancedPasteSettings.Properties.PasteAIConfiguration = newValue;
SyncProviderActiveFlags(newValue);
SubscribeToPasteAIConfiguration(newValue);
OnPropertyChanged(nameof(PasteAIConfiguration));
@@ -587,11 +597,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
.Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut])
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
public bool IsAdditionalActionConflictingCopyShortcut =>
_additionalActions.GetAllActions()
.OfType<AdvancedPasteAdditionalAction>()
.Select(additionalAction => additionalAction.Shortcut)
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
public bool IsAdditionalActionConflictingCopyShortcut
{
get
{
var shortcuts = _additionalActions.GetAllActions()
.OfType<AdvancedPasteAdditionalAction>()
.Select(additionalAction => additionalAction.Shortcut)
.ToList();
// The coaching shortcut is a separately-registered hotkey; include it when active.
var fixSpelling = _additionalActions.FixSpellingAndGrammar;
if (fixSpelling.CoachingEnabled && fixSpelling.CoachingShortcut is { Code: not 0 })
{
shortcuts.Add(fixSpelling.CoachingShortcut);
}
return shortcuts.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
}
}
private void NotifySettingsChanged()
{
@@ -781,6 +805,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
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)
@@ -1083,7 +1126,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
SaveAndNotifySettings();
if (e.PropertyName == nameof(AdvancedPasteAdditionalAction.Shortcut))
if (e.PropertyName is nameof(AdvancedPasteAdditionalAction.Shortcut)
or nameof(AdvancedPasteAdditionalAction.CoachingShortcut)
or nameof(AdvancedPasteAdditionalAction.CoachingEnabled))
{
OnPropertyChanged(nameof(IsAdditionalActionConflictingCopyShortcut));
}
@@ -1324,7 +1369,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
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 +1456,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
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 +1478,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
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 +1501,32 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
pasteConfig.Providers ??= new ObservableCollection<PasteAIProviderDefinition>();
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))