Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
2990aad
this didn't work and no one knows why
zadjii-msft Aug 6, 2025
c198cea
Merge remote-tracking branch 'origin/main' into dev/migrie/f/properti…
zadjii-msft Aug 7, 2025
95cfdbb
oh god oh god what have i done
zadjii-msft Aug 7, 2025
7eaa701
not this
zadjii-msft Aug 7, 2025
88111e2
Revert "not this"
zadjii-msft Aug 7, 2025
73bc438
I keep thinking it can't get any stupider and they keep bringing me back
zadjii-msft Aug 7, 2025
9066020
warning - extreme stupid levels detected
zadjii-msft Aug 7, 2025
3ed3bb6
this can be shipped
zadjii-msft Aug 7, 2025
36f8521
:shipit:
zadjii-msft Aug 7, 2025
940e71f
stupid levels returning to nominal values
zadjii-msft Aug 8, 2025
f3588e7
more spec cleanup
zadjii-msft Aug 8, 2025
dd1a60c
really VS really
zadjii-msft Aug 8, 2025
9ecd374
spel
zadjii-msft Aug 8, 2025
c5db9d2
rename; use IDictionary instead of IPropertySet
zadjii-msft Aug 11, 2025
62c2c60
Merge remote-tracking branch 'origin/main' into dev/migrie/f/properti…
zadjii-msft Aug 12, 2025
b1432c7
more updates
zadjii-msft Aug 13, 2025
a78ce38
Revert "more updates"
zadjii-msft Aug 13, 2025
e7dd3ac
Reapply "more updates"
zadjii-msft Aug 13, 2025
cc4fd8f
why does trimming & AOT this break this API?
zadjii-msft Aug 13, 2025
6c4171e
nah. I'm gonna do my own thing
zadjii-msft Aug 14, 2025
8183054
i'm still fighting, i don't fear i've lost
zadjii-msft Aug 14, 2025
962dc35
more notes, and more notes
zadjii-msft Aug 15, 2025
eaa9cbf
more cleanup
zadjii-msft Aug 15, 2025
3565582
subtle boxes
zadjii-msft Aug 15, 2025
08b0f30
spel
zadjii-msft Aug 15, 2025
9b54e96
Merge remote-tracking branch 'origin/main' into dev/migrie/s/token-se…
zadjii-msft Sep 4, 2025
e55bc8c
this feels like i'm getting somewhere
zadjii-msft Sep 5, 2025
12b8973
i hate this I think
zadjii-msft Sep 8, 2025
2970fc5
I kinda like this?
zadjii-msft Sep 8, 2025
c2ced6d
a lot of notes here. Maybe we should write real code now
zadjii-msft Sep 12, 2025
8d10739
code that compiles
zadjii-msft Sep 12, 2025
89c9cf1
Merge remote-tracking branch 'origin/main' into dev/migrie/s/token-se…
zadjii-msft Sep 12, 2025
e557e05
Wire it up to the UI, painfully, but it works
zadjii-msft Sep 12, 2025
2671a9d
Alright now, wasn't that fun? Let's try something else
zadjii-msft Sep 12, 2025
e970c18
Make some things observable
zadjii-msft Sep 13, 2025
580ae74
wire everything up for buttons
zadjii-msft Sep 13, 2025
a855905
more observable
zadjii-msft Sep 14, 2025
70a7865
react as soon as the text changes
zadjii-msft Sep 14, 2025
acc0965
once agagin, i am asking to support actions
zadjii-msft Sep 14, 2025
bcc30fc
Debug printing actions
zadjii-msft Sep 15, 2025
bd2dc3a
add a photo picker
zadjii-msft Sep 15, 2025
ea11b87
add a tag for LLM slop
zadjii-msft Sep 15, 2025
12f9549
focuses search slightly better
zadjii-msft Sep 15, 2025
446b0d4
one fewer focus
zadjii-msft Sep 15, 2025
12ef613
Focus the next param on enter
zadjii-msft Sep 22, 2025
6b144aa
spec updates
zadjii-msft Sep 22, 2025
5bb1dbe
Merge branch 'main' into dev/migrie/s/token-search-page
zadjii-msft Oct 10, 2025
bce5bc9
there's no way that just works, right?
zadjii-msft Nov 17, 2025
ba9a417
Revert "there's no way that just works, right?"
zadjii-msft Nov 19, 2025
85a1a8d
make it work again
zadjii-msft Nov 20, 2025
cdfda6c
Merge remote-tracking branch 'origin/main' into dev/migrie/s/token-se…
zadjii-msft Nov 20, 2025
c2b6f9d
Turns out we changed the type of parameter passed
zadjii-msft Nov 20, 2025
4db8236
remove a bunch of files I don't need
zadjii-msft Nov 20, 2025
6bb446b
dedupe files
zadjii-msft Nov 20, 2025
0ae3502
works again
zadjii-msft Nov 20, 2025
39eb8ab
code cleanup
zadjii-msft Nov 20, 2025
459546e
nit
zadjii-msft Nov 20, 2025
84af471
Resurrect the suggestion page
zadjii-msft Nov 21, 2025
1e71527
doc updates
zadjii-msft Nov 21, 2025
697736c
Final PR cleanup
zadjii-msft Nov 21, 2025
40d18fd
@ me spellbot
zadjii-msft Nov 21, 2025
840de10
spellbot strikes again
zadjii-msft Nov 21, 2025
f7a5169
Visual changes to param
niels9001 Nov 25, 2025
7b41be3
Merge remote-tracking branch 'origin/main' into dev/migrie/s/token-se…
zadjii-msft Dec 2, 2025
b7a034c
Merge remote-tracking branch 'origin/main' into dev/migrie/s/token-se…
zadjii-msft Dec 9, 2025
040d4a9
Merge branch 'dev/migrie/s/token-search-page' into dev/migrie/f/param…
zadjii-msft Apr 23, 2026
e4fd80a
Post-merge API compat + path fixes for parameters feature
zadjii-msft Apr 23, 2026
8de501f
spell
zadjii-msft Apr 23, 2026
e77c3c8
toolkit updates
zadjii-msft Apr 24, 2026
d12b1b9
Merge remote-tracking branch 'origin/main' into dev/migrie/f/paramete…
zadjii-msft May 12, 2026
39bfe13
Address review comments: fix silent exception swallowing, remove Debu…
MuyuanMS May 13, 2026
aef9e23
fix build
zadjii-msft May 13, 2026
4a121ed
Merge branch 'dev/migrie/f/parameters-2026' of https://github.com/mic…
zadjii-msft May 13, 2026
09b7140
spel
zadjii-msft May 13, 2026
196984a
Merge remote-tracking branch 'origin/main' into dev/migrie/f/paramete…
zadjii-msft May 13, 2026
caa8652
async Task, not void
zadjii-msft May 13, 2026
96e26d1
needs cleanup, not just dispose
zadjii-msft May 13, 2026
9096a5e
lock _isInitialized
zadjii-msft May 13, 2026
f2f1f93
log & throw when it isn't a string
zadjii-msft May 13, 2026
d096245
yea, NeedsValue was weird
zadjii-msft May 14, 2026
dacf93f
gotta do viewmodel updates in order doncha know
zadjii-msft May 14, 2026
53e07d2
a bunch more feedback
zadjii-msft May 14, 2026
5fbf95c
Merge remote-tracking branch 'origin/main' into dev/migrie/f/paramete…
zadjii-msft May 14, 2026
9ad7dea
CmdPal: Add support for List parameters (#47885)
zadjii-msft May 15, 2026
dc0ced3
Merge remote-tracking branch 'origin/main' into dev/migrie/f/paramete…
zadjii-msft May 15, 2026
b4a4e38
Merge remote-tracking branch 'origin/main' into dev/migrie/f/paramet…
zadjii-msft May 15, 2026
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
8 changes: 8 additions & 0 deletions .github/actions/spell-check/allow/code.txt
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ onefuzz
# NameInCode
leilzh
mengyuanchen
contoso

# DllName
testhost
Expand Down Expand Up @@ -346,6 +347,7 @@ WINDOWPOS
WINEVENTPROC
WORKERW
FULLSCREENAPP
DEFAULTTONEAREST

# COM/WinRT interface prefixes and type fragments
BAlt
Expand Down Expand Up @@ -380,6 +382,12 @@ YYY
# Unicode
precomposed

# names of characters
zwsp

# mermaid
autonumber

# GitHub issue/PR commands
azp
feedbackhub
Expand Down
4 changes: 4 additions & 0 deletions PowerToys.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Actions/Microsoft.CmdPal.Ext.Actions.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ A markdown page is a page inside of command palette that displays markdown conte

```csharp
interface IMarkdownPage requires IPage {
String[] Bodies(); // TODO! should this be an IBody, so we can make it observable?
String[] Bodies();
IDetails Details();
IContextItem[] Commands { get; };
}
Expand Down
185 changes: 184 additions & 1 deletion doc/devdocs/modules/cmdpal/initial-sdk-spec/initial-sdk-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ functionality.
- [Status messages](#status-messages)
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
- [Addenda I: API additions (ICommandProvider2)](#addenda-i-api-additions-icommandprovider2)
- [Addenda II: Commands with Parameters](#addenda-ii-commands-with-parameters)
- [String parameters](#string-parameters)
- [Command parameters - Invokable Commands](#command-parameters---invokable-commands)
- [Command parameters - List Commands](#command-parameters---list-commands)
- [Examples](#examples)
- [Addenda III: Rich Search (DRAFT)](#addenda-iii-rich-search-draft)
- [Nov 2025 status](#nov-2025-status)
- [Addenda IV: Dock bands](#addenda-iv-dock-bands)
- [Pinning nested commands to the dock (and top level)](#pinning-nested-commands-to-the-dock-and-top-level)
- [Class diagram](#class-diagram)
Expand Down Expand Up @@ -2048,6 +2055,183 @@ Fortunately, we can put all of that (`GetApiExtensionStubs`,
developers won't have to do anything. The toolkit will just do the right thing
for them.

## Addenda II: Commands with Parameters

Extensions will often want to provide commands that accept parameters from the
user.

To support this, we're adding a new page type. The `IParametersPage` is a page
that allows an extension to define a set of parameters that the user can fill.
These parameters can be of different types, such as:
* Labels: static text that provides context or instructions.
* String parameters: text input fields where the user can type a string.
* Command parameters: interactive fields that allow the user to select from a
list of predefined commands, or just press a button to select an input.

Interleaving labels with parameters allows extensions to create rich, guided
input forms for their commands. These are a more lightweight solution than the
current adaptive card content.

```csharp
[uuid("a2590cc9-510c-4af7-b562-a6b56fe37f55")]
interface IParameterRun requires INotifyPropChanged
{
};

interface ILabelRun requires IParameterRun
{
String Text{ get; };
};

interface IParameterValueRun requires IParameterRun
{
String PlaceholderText{ get; };
Boolean NeedsValue{ get; }; // TODO! name is weird
};

interface IStringParameterRun requires IParameterValueRun
{
String Text{ get; set; };

// TODO! do we need a way to validate string inputs?
};

interface ICommandParameterRun requires IParameterValueRun
{
String DisplayText{ get; };
ICommand GetSelectValueCommand(UInt64 hostHwnd);
IIconInfo Icon{ get; }; // ? maybe
};

interface IParametersPage requires IPage
{
IParameterRun[] Parameters{ get; };
IListItem Command{ get; };
};
```

When we open a `IParametersPage`, we will render the `Parameters` in the search
box. We'll move focus to the first `IParameterRun` that is not a `ILabelRun`.
What those interactions looks like depends on the type of `IParameterRun`.

There are three basic types of inputs: strings, invokable commands, and lists.
Strings are a special case that doesn't require a command to set the value.
Lists and invokable commands are picked based on the type of the
`SelectValueCommand`. Each of these are detailed below.

When all the parameters have `NeedsValue` set to `false`, we will display a
single item to the user - the `Command` item.

### String parameters

These are rendered as a text box within the search box. The user can type into
it. Focus is moved to the next parameter when the user presses Enter or tab.

### Command parameters - Invokable Commands

These are used when the `SelectValueCommand` is an `IInvokableCommand`.

These are rendered as a button within the search box. The button text is
`DisplayText` if it is set. If it is not, we will display the
`PlaceholderText`. If the user clicks the button, we invoke the
`SelectValueCommand` (and ignore the `CommandResult`).

This is good for file pickers, date pickers, color pickers, etc. Anything that
requires a custom UI to pick a value.

When the extension has picked a value, it should set the `NeedsValue` to false.
The extension can also set the `DisplayText` and `Icon` to reflect the chosen value.

When the user presses enter with the button focused, we will also invoke the
`SelectValueCommand`.

When the user presses tab, we will move focus to the next parameter.

If the `NeedsValue` property is changed to `false` while it's focused, we will
move focus to the next parameter.

### Command parameters - List Commands

These are used when the `SelectValueCommand` is an `IListPage` - both static and
dynamic lists work similarly.

These are rendered as a text box within the search box. When the user focuses
the text box, we will display the items from the `IListPage` in the body of
CmdPal. The user can then type to filter the list. This filtering will work the
same way as any other list page in CmdPal - CmdPal will filter static lists, or
pass the query to a dynamic list.

The items in this list should all be `IListItem` objects with
`IInvokableCommands`. Putting a `IPage` into one of these items will cause the
user to navigate away from the parameters page, which would probably be
unexpected.

When the user picks an item from the list, the extension should handle that
command by bubbling an event up to the `CommandRun`, and setting the `Value`,
`DisplayText`, and `Icon` properties, and setting `NeedsValue` to false.

When the user presses enter with the text box focused, we will invoke the
command of the selected item in the list.

When the user presses tab, we will move focus to the next parameter.

If the `NeedsValue` property is changed to `false` while it's focused, we will
move focus to the next parameter.

### Examples

Lets say you had a command like "Create a note \${title} in \${folder}".
`title` is a string input, and `folder` is a static list of folders.

The extension author can then define a `IParametersPage` with four runs in it:
* A `ILabelRun` for "Create a note"
* A `IStringParameterRun` for the `title`
* A `ILabelRun` for "in"
* A `ICommandParameterRun` for the `folder`. The `Command` will be a
`IListPage`, where the items are possible folders

In this example, the user can pick the "create note" command, then type the
title, hit enter/tab, and then pick a folder from the list, then hit enter to
run the command.

Samples for the parameters page are implemented over in
[the sample extension](../../ext/SamplePagesExtension/Pages/ParameterSamples.cs)


## Addenda III: Rich Search (DRAFT)

> [!NOTE]
> _Mike_: Rich search and parameters were prototyped together, but ultimately we used two different solutions.
>
> Currently, we have a dummy implementation of draft C (ZWSP tokens), but without full API changes. Detailed [below](#nov-2025-status).
Extensions will often want to provide rich search experiences for their users.

This addenda is broken into multiple draft specs currently. These represent
different approaches to the same goals.

* **A**: [Rich Search Box](./drafts/RichSearchBox-draft-A.md)
* **B**: [Prefix Search](./drafts/PrefixSearch-draft-B.md)
* **C**: [ZWSP tokens](./drafts/PlainRichSearch-draft-C.md)

### Nov 2025 status

As of Nov 2025, we're implementing a simple version of draft C in the host.

In this version, if the extension implements `IDynamicListPage`, and also
implements `IExtendedAttributesProvider`, then they can set the `TokenSearch`
property. This will enlighten CmdPal to treat ZWSP-separated tokens in the
search text specially.

For an example, see
[this sample implementation](../../ext/SamplePagesExtension/Pages/SampleSuggestionsPage.cs).

In my head, I am still leaning towards a more full-featured version of draft C,
but with full CommandItem's in the `ISearchUpdateArgs` instead of just strings.
We'd almost need a new page type to support that, where the extension can add
`ICommandItem`s to the search box directly.

## Addenda IV: Dock bands

The "dock" is another way to surface commands to the user. This is a
Expand Down Expand Up @@ -2158,7 +2342,6 @@ because that method is was designed for two main purposes:
In neither of those scenarios was the full "display" of the item needed. In
pinning scenarios, however, we need everything that the user would see in the UI
for that item, which is all in the `ICommandItem`.

## Class diagram

This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.)
Expand Down
1 change: 1 addition & 0 deletions src/modules/cmdpal/CommandPalette.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UI.ViewModels.UnitTests\\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UITests\\Microsoft.CmdPal.UITests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Actions\\Microsoft.CmdPal.Ext.Actions.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Apps\\Microsoft.CmdPal.Ext.Apps.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Bookmark\\Microsoft.CmdPal.Ext.Bookmarks.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Calc\\Microsoft.CmdPal.Ext.Calc.csproj",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.CmdPal.Core.Common.Messages;
namespace Microsoft.CmdPal.Common.Messages;

/// <summary>
/// Message to request hiding the window.
/// </summary>
public partial record HideWindowMessage();
public partial class GetHwndMessage
{
public nint Hwnd { get; set; } = 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public CommandPalettePageViewModelFactory(TaskScheduler scheduler, IContextMenuF
MainListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsRootPage = !nested, IsMainPage = true },
IListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsRootPage = !nested },
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host, providerContext),
IParametersPage paramsPage => new ParametersPageViewModel(paramsPage, _scheduler, host, providerContext, _contextMenuFactory),
_ => null,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ protected void FetchProperty(string propertyName)
var iconInfo = model.Icon;
Icon = new(iconInfo);
Icon.InitializeProperties();
break;
case nameof(Properties):
UpdatePropertiesFromExtension(model as IExtendedAttributesProvider);

break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ public partial class ListViewModel : PageViewModel, IDisposable

public bool IsMainPage { get; init; }

public bool IsTokenSearch { get; private set; }

public bool HasCustomDebounceLogic => IsMainPage;

private bool _isDynamic;
Expand Down Expand Up @@ -957,6 +959,11 @@ public override void InitializeProperties()
Filters?.InitializeProperties();
UpdateProperty(nameof(Filters));

if (model is IExtendedAttributesProvider haveProperties)
{
LoadExtendedAttributes(haveProperties.GetProperties().AsReadOnly());
}

FetchItems(true);
model.ItemsChanged += Model_ItemsChanged;
}
Expand All @@ -972,6 +979,17 @@ public override void InitializeProperties()
};
}

private void LoadExtendedAttributes(IReadOnlyDictionary<string, object> properties)
{
// Check if this is a token page
if (properties.TryGetValue("TokenSearch", out var isTokenSearchObj) &&
isTokenSearchObj is bool isTokenSearch)
{
IsTokenSearch = isTokenSearch;
UpdateProperty(nameof(IsTokenSearch));
}
}

public void LoadMoreIfNeeded()
{
var model = _model.Unsafe;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.CommandPalette.Extensions;

namespace Microsoft.CmdPal.UI.ViewModels.Messages;

public record FocusParamMessage(ParameterValueRunViewModel Parameter);
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ public override void InitializeProperties()
Icon = new(page.Icon);
Icon.InitializeProperties();

HasSearchBox = page is IListPage;
HasSearchBox = (page is IListPage) || (page is IParametersPage);

// Let the UI know about our initial properties too.
UpdateProperty(nameof(Name));
Expand Down
Loading
Loading