Files
Boliang ZhangandCopilot 31de3688ec fix(advancedpaste): migrate CLI to System.CommandLine 2.0 (#51230)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

Migrate AdvancedPaste.CLI from the System.CommandLine beta4 API to the
centrally pinned 2.0.1 API. This fixes the main-branch compilation
failure after #50884, which upgraded the dependency after #50834
introduced this CLI.

Preserve the existing commands, parameter names and aliases, help text,
JSON output, clipboard defaults, and exit codes. Add regression coverage
for aliases and help-like option values.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: N/A (build regression involving #50834 and #50884)
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: N/A

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

### Cause and scope

[Main CI
390858](https://dev.azure.com/shine-oss/PowerToys/_build/results?buildId=390858)
and
[390863](https://dev.azure.com/shine-oss/PowerToys/_build/results?buildId=390863)
fail on both architectures with missing `System.CommandLine.IO`,
`IConsole`, and `IStandardStreamWriter`. AdvancedPaste.CLI also still
used beta4 option constructors, command registration, parsing, and
value/result lookup APIs.

- `src/modules/AdvancedPaste/AdvancedPaste.CLI/Program.cs`: use
`Options`/`Subcommands`, `RootCommand.Parse`,
`ParseResult.GetValue`/`GetResult`, and
`InvocationConfiguration.Output`/`Error`. Define option names/aliases
separately from `Description` and remove the obsolete console adapter.
- Keep recognizing help-like values belonging to an option, rather than
interpreting a prompt or path such as `-h` as a help request.
-
`src/modules/AdvancedPaste/AdvancedPaste.Cli.UnitTests/ProgramTests.cs`:
cover both action aliases, long/short file options, all existing help
aliases, and help-like prompt values. Existing JSON, error,
cancellation, policy, and clipboard-adapter tests remain intact.
- `doc/devdocs/modules/advancedpaste.md`: document the parser API and
the help-token invariant.

No dependency downgrade, new binaries, installer changes, policy
changes, test suppression, or pipeline changes. User-facing text is
unchanged; the localization checkbox denotes preservation of existing
behavior. New-binary and external user-documentation checklist items are
N/A.

This PR targets **main only**. It does not cherry-pick to stable, queue
a signed release, or publish a release.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

### Build scope and environment limitation

The ordinary documented build command was attempted:

```powershell
Set-Location 'D:\PT102-CommandLineFix\src\modules\AdvancedPaste\AdvancedPaste.Cli.UnitTests'
& 'D:\PT102-CommandLineFix\tools\build\build.ps1' -Platform x64 -Configuration Release -Path . -ExtraArgs '/p:CIBuild=false'
```

**Blocked before source compilation, exit 1:** the local Visual Studio
installation cannot resolve `Microsoft.NET.Sdk`. Explicitly selecting
the installed SDK then fails in `ProcessFrameworkReferences` with
MSB4018 loading `Microsoft.Deployment.DotNet.Releases, Version=2.0.0.0`.
A task-local .NET 10.0.303 SDK can restore the real project graph, but
its Core MSBuild host cannot execute the native VC tasks. No tracked
build settings or system installation were changed to bypass these
failures.

To validate the changed code despite that host issue, rebuilt the
**actual CLI and unit-test `.csproj` files**, with their repository
analyzers and warnings-as-errors, using unchanged dependency binaries
from successful [CI
390854](https://dev.azure.com/shine-oss/PowerToys/_build/results?buildId=390854),
source `26b99a0a01af48b87a6aacd341e2c355ea3c6d1e`. The source of the
reused managed dependency projects is unchanged from that build to this
PR's base `863ff53e9b87790cf2885d5ef527587534e1fecf`.

An **uncommitted, local-only** `ReuseCiDependencies.targets` replaces
project references with those dependency assemblies. The CLI itself and
all CLI test sources were compiled from this worktree against
System.CommandLine **2.0.1**; neither the parser nor the CLI
implementation was stubbed. This is dependency-reuse validation, **not**
a successful clean build of the full dependency graph.

```powershell
$repo = 'D:\PT102-CommandLineFix'
$validation = "$repo\artifacts\commandline-fix"
$dotnet = "$validation\dotnet\dotnet.exe"
$env:DOTNET_ROOT = "$validation\dotnet"
$env:MSBuildEnableWorkloadResolver = 'false'
$buildArgs = @(
    '/t:Build', '/p:Platform=x64', '/p:Configuration=Release',
    "/p:SolutionDir=$repo\", '/p:CIBuild=false', '/p:SkipLongPathsCheck=true',
    '/p:BuildProjectReferences=false', '/p:DisableTransitiveProjectReferences=true',
    "/p:CustomAfterMicrosoftCommonTargets=$validation\ReuseCiDependencies.targets",
    '/v:minimal', '/nologo'
)
& $dotnet msbuild "$repo\src\modules\AdvancedPaste\AdvancedPaste.CLI\AdvancedPaste.CLI.csproj" @buildArgs
& $dotnet msbuild "$repo\src\modules\AdvancedPaste\AdvancedPaste.Cli.UnitTests\AdvancedPaste.Cli.UnitTests.csproj" @buildArgs
```

**Both passed, exit 0.** `SkipLongPathsCheck` is the existing developer
option; the machine-wide long-path registry setting was not modified.

### Tests and executable smoke checks

```powershell
Set-Location "$repo\x64\Release\tests\AdvancedPaste.Cli.UnitTests"
& $dotnet '.\AdvancedPaste.Cli.UnitTests.dll' --filter 'FullyQualifiedName~ProgramTests' --report-trx
```

**Passed, exit 0: 91/91, zero skipped.**

```powershell
& $dotnet '.\AdvancedPaste.Cli.UnitTests.dll' --report-trx
```

**Passed, exit 0: 151/151, zero skipped**, including the nine additional
regression cases.

```powershell
& "$repo\src\modules\AdvancedPaste\AdvancedPaste.CLI\SmokeTest.ps1" `
    -Executable "$repo\x64\Release\WinUI3Apps\PowerToys.AdvancedPaste.Cli.exe"
```

**Passed, exit 0:** existing executable smoke checks for help,
stdin/stdout, file conversion, JSON output, conflicting input modes, and
unsupported actions. Unchanged runtime dependencies and
`PowerToys.AdvancedPaste.pri` were supplied from the same successful CI
build. No real clipboard writes or AI-provider calls were performed.

```powershell
git -C D:\PT102-CommandLineFix diff --check
```

**Passed.**

ARM64 compilation, UI automation, a clean native dependency build, and
an official signed release build were **not completed locally**. Public
PR CI remains the full integration gate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 19:14:34 +08:00

14 KiB

Advanced Paste

Public overview - Microsoft Learn

All Issues
Bugs
Pull Requests

Overview

Advanced Paste is a PowerToys module that provides enhanced clipboard pasting with formatting options and additional functionality.

Implementation Details

Source code

TODO: Add implementation details

Headless CLI

PowerToys.AdvancedPaste.CLI.exe runs Advanced Paste actions without starting the Advanced Paste UI or communicating with Runner. It loads the same shared action engine, a startup snapshot of settings, policy, credential-vault entries, AI providers, OCR, and media-transcoding implementation used by the UI; live settings-file watching is disabled for the one-shot CLI process.

Get-Clipboard | PowerToys.AdvancedPaste.CLI.exe transform --action plain-text --stdin --stdout
PowerToys.AdvancedPaste.CLI.exe transform --action markdown --input notes.html --output notes.md
PowerToys.AdvancedPaste.CLI.exe transform --action image-to-text --input screenshot.png --stdout
PowerToys.AdvancedPaste.CLI.exe transform --action transcode-to-mp3 --input recording.mp4 --output recording.mp3
PowerToys.AdvancedPaste.CLI.exe transform --action paste-with-ai --prompt "Summarize this" --clipboard
PowerToys.AdvancedPaste.CLI.exe transform --custom-action 3 --clipboard
# After copying a CSV file in Explorer, read it from the default clipboard input.
PowerToys.AdvancedPaste.CLI.exe transform --action json --stdout

Input defaults to the clipboard; use at most one of --input <path>, --stdin, or --clipboard to select a source explicitly. Piped input requires --stdin; redirecting standard input does not override the clipboard default. Output also defaults to the clipboard; use at most one of --output <path>, --stdout, or --output-clipboard. Clipboard output sets content but never simulates paste keys. Text inputs are limited to 16,777,216 characters and raw input, prompts, and output are never written to the CLI log. --format remains an alias for --action.

Clipboard modes use the Win32/OLE clipboard so they do not require a foreground app window. Like the Windows clipboard itself, they require an interactive user session; use file or standard-stream modes for services and session-0 automation.

Built-in actions are plain-text, markdown, json, fix-spelling-and-grammar, image-to-text, paste-as-txt-file, paste-as-png-file, paste-as-html-file, transcode-to-mp3, transcode-to-mp4, and paste-with-ai. Use actions list to include configured custom actions. The CLI honors the Advanced Paste enabled policy and AI actions honor the configured provider, AI-related GPO, moderation, and credentials; --provider <id> selects another configured provider.

Root --help and transform --help describe every built-in action, input/output defaults, AI/custom-action setup, examples, size limits, cancellation, and exit codes without loading settings or providers. actions list includes descriptions; its JSON array uses keys name, kind, id, requiresPrompt, and description. Saved custom actions are discovered through actions list, since they depend on the user's configuration.

AI providers

A provider is an AI service and model configured in PowerToys Settings > Advanced Paste, such as Ollama with a local model or OpenAI with a cloud model. Provider IDs are generated per configured entry; there is no fixed list of IDs, and names such as Ollama are not IDs.

PowerToys.AdvancedPaste.CLI.exe providers list
PowerToys.AdvancedPaste.CLI.exe providers list --json
PowerToys.AdvancedPaste.CLI.exe transform --action paste-with-ai --prompt "Summarize in one sentence" --stdin --stdout --provider <id-from-list>

providers list shows each configured ID, service/model, default selection, and policy status without making AI requests or reading the clipboard. It lists configuration, not a connectivity or credential-health check; it also works when AI is turned off in settings. JSON output is an array with stable keys id, name, serviceType, model, isDefault, and isAllowedByPolicy. API keys, endpoints, and system prompts are not included.

--provider applies only to paste-with-ai, fix-spelling-and-grammar, and --custom-action. It overrides the saved action's provider or the default provider for that invocation, including nested Advanced AI transformations and cached action chains. Unknown explicit IDs are rejected rather than falling back to another provider. Without the option, the saved action's configured provider is used when present, otherwise the default provider. Plain-text, Markdown, JSON, OCR, and media conversion do not use this AI-provider option.

Configure providers and credentials in Settings; the CLI does not enable AI, create providers, sign in, install local models, or accept API keys. Local Ollama needs a running server and an installed model with a matching model name; cloud services need their own API access and can incur charges. A GitHub Copilot subscription is not an Advanced Paste provider.

Text files are exposed as text/HTML, not as image/file attachments; malformed UTF-8 and binary files retain their original file representation. With default clipboard input or explicit --clipboard, a single file copied in Explorer is read using the same bounded file reader as --input, enabling copied CSV-to-JSON and HTML-to-Markdown conversions. Existing clipboard text, HTML, or bitmap takes precedence; folders and multiple-file selections are not converted to text or combined. Markdown and custom AI actions also accept HTML-only clipboard content. OpenAI moderation is skipped, with a warning in the log, when OpenAI or online AI is forbidden by policy; an otherwise permitted Azure provider can still run.

Generated file output is cleaned after copying to an explicit output path. Older owned temporary directories are cleaned only when clipboard input provides a snapshot (default or explicit --clipboard), excluding directories containing current clipboard files or the selected output. Cleanup uses paths from the original file-drop snapshot even after text-file input has been converted to text/HTML. File/stdin commands and action listing never read the clipboard for cleanup.

For transform, --json emits one UTF-8 JSON result envelope on stdout (status, action, resultKind, outputPath, outputClipboard, and optional output) or one error envelope on stderr (status, code, message, and usage for argument errors). actions list --json emits a bare array of action objects (name, kind, optional id, and requiresPrompt). The stable exit codes are 0 for success, 1 for input, I/O, cancellation, provider, clipboard, or transformation failures, and 2 for parser/argument errors.

Run src\modules\AdvancedPaste\AdvancedPaste.CLI\SmokeTest.ps1 after building the CLI to exercise help, stdin/stdout, file input/output, JSON output, and failing argument paths against the built executable.

The CLI uses the centrally pinned System.CommandLine 2.0 API. Register options and subcommands through Options and Subcommands, read parsed values with ParseResult.GetValue, and route help through InvocationConfiguration.Output and Error. Keep option-value token checks when detecting help so a prompt or file path such as -h is not treated as a help request. AdvancedPaste.Cli.UnitTests covers these cases, command aliases, JSON output, and exit codes.

Paste with AI Preview

The "Show preview" setting (ShowCustomPreview) controls whether AI-generated results are displayed in a preview window before pasting. The preview feature does not consume additional AI credits—the preview displays the same AI response that was already generated, cached locally from a single API call.

The implementation flow:

  1. User initiates "Paste with AI" action
  2. A single AI API call is made via ExecutePasteFormatAsync
  3. The result is cached in GeneratedResponses
  4. If preview is enabled, the cached result is displayed in the preview UI
  5. User can paste the cached result without any additional API calls

See the ExecutePasteFormatAsync(PasteFormat, PasteActionSource) method in OptionsViewModel.cs for the implementation.

Debugging

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 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) 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

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:

$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:

& '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

Settings

Setting Description
ShowCustomPreview When enabled, shows AI-generated results in a preview window before pasting. Does not affect AI credit consumption.

Future Improvements

TODO: Add potential future improvements