Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ aspirational
Authenticode
AUTOLISTEN
azureedge
Bawa
binlog
binver
bstr
Expand Down Expand Up @@ -80,6 +81,7 @@ dotnet
downloaders
dsx
DWORDLONG
EApp
emoji
ENDDIALOG
ensureandinsert
Expand Down
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ The solution uses:
- vcpkg for C++ dependencies
- NuGet for C++ and .NET dependencies

CI uses `/p:PreferredToolArchitecture=x64` to avoid 32-bit linker memory limits without changing the target architecture. Use the same setting for command-line Release builds.

### Running/Debugging

1. Deploy solution: Build > Deploy Solution
Expand Down
15 changes: 15 additions & 0 deletions doc/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## New Features

### Interactive package selection (experimental)

Set `experimentalFeatures.interactivePackageSelection` to `true` in settings to enable numbered choices when multiple packages match a single-package `install`, `show`, or `download` command in an interactive terminal. Enter a package number to continue or `0` to cancel.

This feature is disabled by default. Redirected and noninteractive callers retain the existing ambiguity error. Use `--id <ID> --exact --source <SOURCE>` to select a package explicitly, or `--disable-interactivity` to prevent prompts.

### Source priority

Source priority is now available without enabling an experimental feature. Use `winget source add --priority <value>` or `winget source edit --name <source> --priority <value>` to configure it. Higher values take precedence; sources with equal priority still require disambiguation when multiple matches remain.

### `--output-locale` argument

Added a new `--output-locale` argument that overrides the language used for WinGet's own output for a single invocation.
Expand Down Expand Up @@ -36,6 +46,11 @@ This change resolves alias failures in non-symlinked scenarios, including cases
Because the alias is now created as an executable hardlink in the install location, command aliases remain available and consistent even when symlink creation is skipped.

### Minor Bug Fixes
* Fixed REST search results bypassing locally verifiable package filters and selectors. Extra manifests are retrieved only for complete source result sets of three or fewer packages. Normalized name/publisher criteria remain unvalidated client-side.
* Fixed installed-package matching incorrectly combining names and publishers from different manifest entries.
* Prevented unrestricted REST searches when a source declares all requested selectors unsupported.
* Prevented REST searches from looping indefinitely when continuation tokens repeat.
* Fixed Unicode case-insensitive prefix matching when case folding changes character lengths.
* Fixed an issue where `winget search --id <msstoreId>` could fail to return a Microsoft Store package unless `--exact` was also provided.
* Updated NUnit to v4
* Fixed a crash (`0x8000ffff`) when using `--disable-interactivity` with the Resume experimental feature enabled during install operations.
Expand Down
14 changes: 7 additions & 7 deletions doc/Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,14 +454,14 @@ This feature enables support for fonts via `winget settings`. The `winget font l
},
```

### sourcePriority
### interactivePackageSelection

This feature enables sources to have a priority value assigned. Sources with a higher priority will appear earlier in search results and will be selected for installing new packages when multiple sources have a matching package.

Note that search result ordering is dependent on several factors, and source priority is the lowest field in that currently (match quality and field are more important).
Enables numbered choices for ambiguous single-package `install`, `show`, and `download` commands, including `show --versions`. Disabled by default.

```json
"experimentalFeatures": {
"sourcePriority": true
},
"experimentalFeatures": {
"interactivePackageSelection": true
},
```

`--disable-interactivity` and redirected input or output still prevent prompting.
127 changes: 127 additions & 0 deletions doc/specs/#5345 - Interactive package selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
author: AmelBawa-msft, GitHub Copilot <Copilot>
created on: 2026-09-28
last updated: 2026-10-01
issue id: 5345
---

# Interactive package selection

For [#5345](https://github.com/microsoft/winget-cli/issues/5345)

## Abstract

Let users resolve ambiguous package matches without restarting their command. An experimental setting enables numbered choices for single-package `install`, `show`, and `download` when interactive input and output are available.

## Inspiration

The same query can match multiple packages, including packages from different sources. Users should be able to choose deliberately without copying an ID into another invocation.

## Solution Design

This feature is disabled by default. Enable it in settings:

```json
{
"experimentalFeatures": {
"interactivePackageSelection": true
}
}
```

Apply existing search matching and source-priority rules first. If multiple candidates remain, eligible CLI call sites opt into selection. Shared workflows remain noninteractive by default.

Display candidates in their existing order with stable, one-indexed numbers. A valid number selects the existing package object without searching again. Preserve command options and continue normal version selection, applicability checks, and agreement handling.

| Situation | Behavior |
| --- | --- |
| Experimental feature disabled (default) | No selection prompts; retain ambiguity errors with refinement guidance. |
| No match | Existing no-match error. |
| One match after existing policy | Continue without prompting. |
| Multiple matches for single-package `install`, `show`, or `download` | Prompt if eligible, including `show --versions`. |
| Truncated results | Retain ambiguity error and request refinement. |
| Invalid or empty input | Explain the valid range and prompt again; no default. |
| `0` | Cancel without acting on a package. |
| Ctrl+C | Cancel immediately, including while waiting for input. |
| EOF or input failure | Report the existing prompt input error. |
| `--disable-interactivity`, interactivity disabled in settings or context | Retain ambiguity error without reading input. |
| `--silent` | Controls installer UI, not selection prompts; normal interactivity rules apply. |
| Redirected input or output, or disabled informational output | Do not prompt. |
| `--no-vt` | Use the same text and numeric input without terminal escape sequences. |
| Multi-package operations, including individual package contexts | No disambiguation prompts, either per package or up front; retain existing ambiguity errors. |
| Upgrade, uninstall, repair, pin, search, list, or completion | No selection prompts. |
| COM API, PowerShell cmdlets, or configuration/DSC | No new prompts or API changes. |

Apart from the experimental setting, the prompt adds no command-line flags, group policies, manifest fields, or schema versions. Existing interactivity controls and the experimental-features group policy apply. Package validation pipelines and manifest authoring tools are unchanged; manifest examples and schema snippets are not applicable.

## UI/UX Design

Reuse the existing ambiguity table with a leading selection number. Show Name, Id, and Source, including Source when all candidates use the same source. For example:

```text
Multiple packages match. Choose one.

# Name Id Source
------------------------------------------
1 Contoso Editor Contoso.Editor winget
2 Contoso Editor Contoso.Editor.Pro winget

Enter a number (1-2), or 0 to cancel: 1
Selected: Contoso Editor [Contoso.Editor]
```

For distinct candidates from different sources:

```text
# Name Id Source
---------------------------------------
1 Contoso Editor Contoso.Editor winget
2 Contoso Editor Contoso.Editor private
```

Each candidate occupies one row, using the existing ambiguity report's package identity and source. Sources grouped within a candidate do not add rows. Use the same introductory text for every command. Do not add another confirmation after selection. Existing consent prompts still apply.

Ambiguity errors include the candidate list and refinement guidance, even when interactive selection is disabled:

```text
Specify a package with --id <ID> --exact --source <SOURCE>.
```

For `configure export`, use `--package-id <ID> --source <SOURCE>` instead.

The original version option remains authoritative.

## Capabilities

### Accessibility

Numeric, line-oriented input works without color, cursor navigation, or arrow keys. All identifying information is text and uses localized labels. Invalid input includes recovery instructions.

### Security

There is no default selection or inferred equivalence. Choosing a package does not accept agreements or bypass existing source, trust, or installer checks.

### Reliability

Selection uses the displayed candidate object rather than re-running a potentially different search. EOF fails explicitly; cancellation never starts installation or download.

### Compatibility

The feature is disabled by default. After opt-in, scripts can preserve ambiguity errors with `--disable-interactivity`, or avoid ambiguity with exact ID and source selectors.

### Performance, Power, and Efficiency

Rendering uses available package metadata, without downloading manifests for display. No terminal redraw loop is required.

## Potential Issues

Long candidate lists require scrolling. Narrow terminals truncate table cells using the existing formatter; users can widen the terminal or cancel and refine their query if candidates are indistinguishable. The selection message includes the full name and ID. Source-defined result truncation must not be presented as a complete selectable list. Matching names and IDs do not prove that packages from different sources are equivalent.

## Future Considerations

Cross-source equivalence heuristics, arrow-key navigation, and selection for installed-package operations are separate changes.

## Resources

- [Package matching background](%23292%20-%20winget%20should%20install%20an%20app%20if%20there%20is%20an%20exact%20match.md)
- [Settings reference](../Settings.md)
12 changes: 12 additions & 0 deletions doc/windows/package-manager/winget/source.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Source supports the following sub-commands for manipulating the sources.
| Sub-command | Description |
|--------------|-------------|
| **add** | Adds a new source. |
| **edit** | Edits an existing source. |
| **list** | Enumerates the list of enabled sources. |
| **update** | Updates a source. |
| **remove** | Removes a source. |
Expand All @@ -52,6 +53,7 @@ The **source** command supports the following options.
| **-n, --name** | The name to identify the source by. |
| **-a, --arg** | The URL or UNC of the source. |
| **-t, --type** | The type of source. |
| **-p, --priority** | Sets the source priority for **add** or **edit**. Higher values take precedence; new sources default to `0`. |
| **-?, --help** | Gets additional help on this command. |
| **--wait** | Prompts the user to press any key before exiting. |
| **--logs, --open-logs** | Open the default logs location. |
Expand All @@ -73,6 +75,16 @@ The **add** sub-command also supports the optional **type** parameter. The **typ
| **Microsoft.PreIndexed.Package** | The type of source \<default>. |
| **Microsoft.Rest** | A Microsoft REST API source. |

## Source priority

To prefer a source when installing packages, set its priority from an elevated terminal:

```powershell
winget source edit --name winget --priority 1
```

If multiple matches remain at the highest priority, refine the search or specify `--source`.

## list

the **list** sub-command enumerates the currently enabled sources. This sub-command also provides details on a specific source.
Expand Down
8 changes: 4 additions & 4 deletions schemas/JSON/settings/settings.schema.0.2.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,13 +339,13 @@
"type": "boolean",
"default": false
},
"resume": {
"description": "Enable support for some commands to resume",
"interactivePackageSelection": {
"description": "Enable interactive selection for ambiguous package matches",
"type": "boolean",
"default": false
},
"sourcePriority": {
"description": "Enable source priority feature",
"resume": {
"description": "Enable support for some commands to resume",
"type": "boolean",
"default": false
}
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@
<ClCompile Include="ExecutionContext.cpp" />
<ClCompile Include="ExecutionProgress.cpp" />
<ClCompile Include="ExecutionReporter.cpp" />
<ClCompile Include="TableOutput.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
Expand Down
3 changes: 3 additions & 0 deletions src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@
<ClCompile Include="ExecutionReporter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="TableOutput.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Commands\HashCommand.cpp">
<Filter>Commands</Filter>
</ClCompile>
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLICore/Argument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ namespace AppInstaller::CLI
case Args::Type::SourceEditExplicit:
return Argument{ type, Resource::String::SourceEditExplicitArgumentDescription, ArgumentType::Standard };
case Args::Type::SourcePriority:
return Argument{ type, Resource::String::SourcePriorityArgumentDescription, ArgumentType::Standard, ExperimentalFeature::Feature::SourcePriority };
return Argument{ type, Resource::String::SourcePriorityArgumentDescription, ArgumentType::Standard };
case Args::Type::SourceTrustLevel:
return Argument{ type, Resource::String::SourceTrustLevelArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help };
case Args::Type::ValidateManifest:
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLICore/Commands/DownloadCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ namespace AppInstaller::CLI
Workflow::OpenSource() <<
Workflow::SearchSourceForSingle <<
Workflow::HandleSearchResultFailures <<
Workflow::EnsureOneMatchFromSearchResult(OperationType::Download) <<
Workflow::EnsureOneMatchFromSearchResult(OperationType::Download, PackageSelectionBehavior::Prompt) <<
Workflow::GetManifestFromPackage(false);
}

Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLICore/Commands/DscPackageResource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ namespace AppInstaller::CLI
}

*SubContext <<
Workflow::SelectSinglePackageVersionForInstallOrUpgrade(Workflow::OperationType::Install, allowDowngrade) <<
Workflow::SelectSinglePackageVersionForInstallOrUpgrade(Workflow::OperationType::Install, Workflow::PackageSelectionBehavior::Disabled, allowDowngrade) <<
Workflow::InstallSinglePackage;

if (SubContext->IsTerminated())
Expand Down
14 changes: 2 additions & 12 deletions src/AppInstallerCLICore/Commands/DscSourceResource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
#include "Resources.h"
#include "Workflows/SourceFlow.h"
#include <winget/RepositorySource.h>
#include <winget/ExperimentalFeature.h>

using namespace AppInstaller::Utility::literals;
using namespace AppInstaller::Repository;
Expand Down Expand Up @@ -111,10 +110,7 @@ namespace AppInstaller::CLI
Output.TrustLevel(TrustLevelStringFromFlags(source.TrustLevel));
Output.Explicit(source.Explicit);

if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourcePriority))
{
Output.Priority(source.Priority);
}
Output.Priority(source.Priority);

std::vector<Repository::SourceDetails> sources;
sources.emplace_back(source);
Expand Down Expand Up @@ -158,7 +154,6 @@ namespace AppInstaller::CLI
std::string priorityString;
if (Input.Priority())
{
THROW_HR_IF(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED, !Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourcePriority));
priorityString = std::to_string(Input.Priority().value());
SubContext->Args.AddArg(Execution::Args::Type::SourcePriority, priorityString);
}
Expand Down Expand Up @@ -202,7 +197,6 @@ namespace AppInstaller::CLI
std::string priorityString;
if (Input.Priority())
{
THROW_HR_IF(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED, !Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourcePriority));
priorityString = std::to_string(Input.Priority().value());
SubContext->Args.AddArg(Execution::Args::Type::SourcePriority, priorityString);
}
Expand Down Expand Up @@ -373,7 +367,6 @@ namespace AppInstaller::CLI
{
if (Input.Priority())
{
THROW_HR_IF(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED, !Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourcePriority));
if (Output.Priority())
{
return Input.Priority().value() == Output.Priority().value();
Expand Down Expand Up @@ -499,10 +492,7 @@ namespace AppInstaller::CLI
output.TrustLevel(TrustLevelStringFromFlags(source.TrustLevel));
output.Explicit(source.Explicit);

if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourcePriority))
{
output.Priority(source.Priority);
}
output.Priority(source.Priority);

WriteJsonOutputLine(context, output.ToJson());
}
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLICore/Commands/InstallCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ namespace AppInstaller::CLI
{
context <<
Checkpoint("PreInstallCheckpoint", {}) << // TODO: Capture context data
InstallOrUpgradeSinglePackage(OperationType::Install);
InstallOrUpgradeSinglePackage(OperationType::Install, PackageSelectionBehavior::Prompt);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/AppInstallerCLICore/Commands/ShowCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@ namespace AppInstaller::CLI
Workflow::OpenSource() <<
Workflow::SearchSourceForSingle <<
Workflow::HandleSearchResultFailures <<
Workflow::EnsureOneMatchFromSearchResult(OperationType::Show) <<
Workflow::EnsureOneMatchFromSearchResult(OperationType::Show, PackageSelectionBehavior::Prompt) <<
Workflow::ReportPackageIdentity <<
Workflow::ShowAppVersions;
}
}
else
{
context <<
GetManifest( /* considerPins */ false) <<
GetManifest( /* considerPins */ false, PackageSelectionBehavior::Prompt) <<
Workflow::ReportManifestIdentity <<
Workflow::SelectInstaller <<
Workflow::ShowManifestInfo;
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLICore/ExecutionContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ namespace AppInstaller::CLI::Execution

private:
DestructionToken m_disableSignalTerminationHandlerOnExit = false;
bool m_isTerminated = false;
std::atomic<bool> m_isTerminated = false;
HRESULT m_terminationHR = S_OK;
size_t m_CtrlSignalCount = 0;
ContextFlag m_flags = ContextFlag::None;
Expand Down
Loading
Loading