chore: first version

This commit is contained in:
JNIH
2026-07-30 18:39:38 +02:00
commit c953ca8460
54 changed files with 4197 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
# Command Palette Extension Copilot Instructions
Concise guidance for AI-assisted development of this Command Palette extension.
## Project Structure
| Folder | Purpose |
|--------|---------|
| `Pages/` | Extension pages (ListPage, ContentPage, DynamicListPage implementations) |
| `Assets/` | Icons and images (StoreLogo.png, etc.) |
| `Properties/` | Launch settings and publish profiles |
| Root `.cs` files | Extension entry point, COM server (Program.cs), and CommandsProvider |
## Key Conventions
- Extensions run **out-of-process** via COM server registration
- `Program.cs` hosts the COM server — do not modify the hosting pattern
- The `CommandProvider` subclass is the entry point for all commands
- Pages are **ICommand** implementations — they can be used anywhere commands are used
- Always **Deploy** (not just Build) to register the MSIX package
- After deploying, use the **Reload** command in Command Palette to refresh
## Build & Deploy
1. In Visual Studio, use **Build > Deploy** (not just Build)
2. In Command Palette, run `Reload` → select "Reload Command Palette extensions"
3. For debugging, run in Debug configuration (F5) and check Output window (Ctrl+Alt+O)
## Source Control
If using git, remove these lines from `.gitignore` (needed for deployment):
- `**/Properties/launchSettings.json`
- `*.pubxml`
## Available Skills
This project includes Copilot skills for common workflows:
- **add-adaptive-card-form** — Create form-based UI with Adaptive Cards
- **add-extension-settings** — Add a settings page to your extension
- **add-dock-band** — Add persistent toolbar widgets
- **add-fallback-commands** — Add catch-all search commands
- **publish-extension** — Publish to Microsoft Store or WinGet
## Documentation
- [Creating an extension](https://learn.microsoft.com/windows/powertoys/command-palette/creating-an-extension)
- [Extension samples](https://learn.microsoft.com/windows/powertoys/command-palette/samples)
- [Extensibility overview](https://learn.microsoft.com/windows/powertoys/command-palette/extensibility-overview)
@@ -0,0 +1,353 @@
---
description: 'Comprehensive guide for developing Command Palette extensions — covers pages, content, commands, items, icons, settings, dock, and debugging'
applyTo: '**/*.cs'
---
# Command Palette Extension Development
Complete reference for building Command Palette (CmdPal) extensions. Extensions run out-of-process as MSIX-packaged COM servers.
## Extension Architecture
### IExtension Interface
The root class implements `IExtension` and `IDisposable`:
```csharp
[Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")]
public sealed partial class MyExtension : IExtension, IDisposable
{
private readonly ManualResetEvent _extensionDisposedEvent;
private readonly MyCommandsProvider _provider = new();
public MyExtension(ManualResetEvent extensionDisposedEvent)
{
_extensionDisposedEvent = extensionDisposedEvent;
}
public object? GetProvider(ProviderType providerType) => providerType switch
{
ProviderType.Commands => _provider,
_ => null,
};
public void Dispose() => _extensionDisposedEvent.Set();
}
```
- Only `ProviderType.Commands` is currently supported
- The `[Guid]` must match the CLSID in `Package.appxmanifest`
### CommandProvider
Override `TopLevelCommands()` to register main commands. Optionally override `FallbackCommands()` and `GetDockBands()`:
```csharp
public partial class MyCommandsProvider : CommandProvider
{
public MyCommandsProvider()
{
DisplayName = "My Extension";
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
}
public override ICommandItem[] TopLevelCommands() => [
new CommandItem(new MyPage()) { Title = DisplayName },
];
}
```
### COM Server (Program.cs)
`Program.cs` hosts the COM server. Do not change this pattern:
```csharp
public class Program
{
[MTAThread]
public static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
{
global::Shmuelie.WinRTServer.ComServer server = new();
ManualResetEvent extensionDisposedEvent = new(false);
var extensionInstance = new MyExtension(extensionDisposedEvent);
server.RegisterClass<MyExtension, IExtension>(() => extensionInstance);
server.Start();
extensionDisposedEvent.WaitOne();
server.Stop();
server.UnsafeDispose();
}
}
}
```
### Package.appxmanifest
Two critical extension registrations must be present:
1. **COM server**`com:ComServer` with matching CLSID and `-RegisterProcessAsComServer` args
2. **App extension**`uap3:AppExtension` with `Name="com.microsoft.commandpalette"` and `CreateInstance ClassId` matching the GUID
The CLSID must be identical in three places: the `[Guid]` attribute, the `com:Class Id`, and the `CreateInstance ClassId`.
## Page Types
### ListPage (Most Common)
Displays a searchable list of items:
```csharp
internal sealed partial class MyPage : ListPage
{
public MyPage()
{
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
Title = "My page";
Name = "Open";
}
public override IListItem[] GetItems() => [
new ListItem(new OpenUrlCommand("https://example.com")) { Title = "Example" },
];
}
```
### DynamicListPage (Search-Reactive)
Responds to search text changes for filtering or live queries:
```csharp
internal sealed partial class MyDynamicPage : DynamicListPage
{
private IListItem[] _filteredItems = [];
public override void UpdateSearchText(string oldSearch, string newSearch)
{
_filteredItems = _allItems
.Where(i => i.Title.Contains(newSearch, StringComparison.OrdinalIgnoreCase))
.ToArray();
RaiseItemsChanged();
}
public override IListItem[] GetItems() => _filteredItems;
}
```
- Supports `Filters` property for category filtering
- Call `RaiseItemsChanged()` after updating items to notify the UI
### ContentPage (Rich Content)
Displays rich content like markdown, forms, or images:
```csharp
internal sealed partial class MyContentPage : ContentPage
{
public override IContent[] GetContent() => [
new MarkdownContent("# Hello\nThis is **markdown**."),
];
}
```
- Can return multiple `IContent` items (mix markdown, forms, images, etc.)
- Supports `Commands` property for context menu items via `CommandContextItem`
## Content Types
| Type | Description |
|------|-------------|
| `MarkdownContent(string)` | Renders markdown with headers, links, code blocks, tables, images |
| `FormContent` | Adaptive Cards forms with `TemplateJson`, optional `DataJson`, and `SubmitForm()` |
| `PlainTextContent(string)` | Plain text; optional `FontFamily.Monospace` and `WrapWords` |
| `ImageContent` | Images with `MaxWidth`/`MaxHeight` constraints |
| `TreeContent` | Hierarchical nested content; override `GetChildren()` for child `IContent[]` |
### MarkdownContent Images
Supports `file:`, `data:` (base64), and `https:` URLs. Image hints control rendering:
```markdown
![alt](https://example.com/img.png?--x-cmdpal-fit=fit&--x-cmdpal-maxwidth=400)
```
### FormContent (Adaptive Cards)
```csharp
internal sealed partial class MyForm : FormContent
{
public MyForm()
{
TemplateJson = """{ "type": "AdaptiveCard", ... }""";
DataJson = """{ "name": "default" }""";
}
public override CommandResult SubmitForm(string payload)
{
var data = JsonSerializer.Deserialize<MyFormData>(payload);
return CommandResult.Dismiss();
}
}
```
- Design cards visually at [adaptivecards.io/designer](https://adaptivecards.io/designer)
- Use `${...}` placeholders in `TemplateJson` bound to `DataJson` properties
## Commands
### InvokableCommand
Actions that do something when activated:
```csharp
internal sealed partial class MyCommand : InvokableCommand
{
public override string Name => "Do it";
public override IconInfo Icon => new("\uE945");
public override CommandResult Invoke()
{
// Do work here
return CommandResult.Dismiss();
}
}
```
### Built-in Command Helpers
| Helper | Purpose |
|--------|---------|
| `OpenUrlCommand(string url)` | Open URL in default browser |
| `CopyTextCommand(string text)` | Copy to clipboard with toast |
| `NoOpCommand()` | Does nothing (placeholder) |
| `AnonymousCommand(Action? action)` | Lambda command; set `Result` property for navigation |
### CommandResult Types
| Result | Behavior |
|--------|----------|
| `CommandResult.Dismiss()` | Hide palette, go home |
| `CommandResult.KeepOpen()` | Stay on current page |
| `CommandResult.Hide()` | Hide palette, keep page state |
| `CommandResult.GoBack()` | Navigate back one page |
| `CommandResult.GoHome()` | Navigate to home page |
| `CommandResult.ShowToast("msg")` | Show toast notification, then dismiss |
| `CommandResult.Confirm(args)` | Show confirmation dialog before proceeding |
## ListItem Properties
```csharp
new ListItem(command)
{
Title = "Display name",
Subtitle = "Secondary text",
Icon = new IconInfo("\uE8A7"),
Tags = [new Tag("label") { Foreground = ColorHelpers.FromRgb(255, 0, 0) }],
Details = new Details
{
Title = "Detail panel",
Body = "**Markdown** body",
HeroImage = IconHelpers.FromRelativePath("Assets\\hero.png"),
Size = ContentSize.Medium,
Metadata = [
new DetailsLink("URL", "https://example.com"),
new DetailsSeparator(),
],
},
MoreCommands = [
new CommandContextItem(deleteCommand)
{
RequestedShortcut = KeyChordHelpers.FromModifiers(
true, false, false, (int)VirtualKey.Delete),
},
],
}
```
## Sections and Grid Layouts
### Sections
Group items under section headers:
```csharp
public override ISection[] GetSections() => [
new Section { Title = "Group A", Items = itemsA },
new Section { Title = "Group B", Items = itemsB },
];
```
### Grid Layouts
Set `GridProperties` on a `ListPage`:
| Layout | Description |
|--------|-------------|
| `GalleryGridLayout()` | Large tiles with title + subtitle |
| `SmallGridLayout()` | Compact grid |
| `MediumGridLayout()` | Medium tiles with title |
## Icons
```csharp
// Segoe Fluent UI icons (most common)
new IconInfo("\uE8A5") // Document
new IconInfo("\uE945") // Lightning bolt
// Emoji
new IconInfo("📂")
// Image from package assets
IconHelpers.FromRelativePath("Assets\\StoreLogo.png")
// Remote URL or SVG
new IconInfo("https://example.com/icon.svg")
// From exe/dll resource
new IconInfo("%systemroot%\\system32\\shell32.dll,3")
```
## Dynamic Updates
- Call `RaiseItemsChanged()` on any page to trigger a UI refresh of its items
- Call `RaisePropertyChanged(propertyName)` for individual property updates (e.g., title)
- For top-level command changes, call `RaiseItemsChanged()` on the `CommandProvider`
- Use `System.Timers.Timer` for periodic background updates
## Status Messages and Toasts
```csharp
// Inline status message (e.g., loading indicator)
var msg = new StatusMessage
{
Message = "Loading...",
State = MessageState.Info,
Progress = new ProgressState { IsIndeterminate = true },
};
ExtensionHost.ShowStatus(msg, StatusContext.Page);
ExtensionHost.HideStatus(msg);
// Transient toast notification
new ToastStatusMessage("Copied to clipboard").Show();
```
## Build & Debug
1. Select **Debug** configuration
2. **Deploy** via Build > Deploy (not just Build) — this registers the MSIX package
3. Press **F5** to launch with debugger attached
4. Use `Debug.Write()` / `Debug.WriteLine()` for diagnostic output
5. Check Output window (**Ctrl+Alt+O**) set to "Debug"
6. In Command Palette, run `Reload` → "Reload Command Palette extensions"
Use the `(Package)` launch profile, not `(Unpackaged)`.
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Building without deploying | Use Build > Deploy so the MSIX package is updated |
| Running "(Unpackaged)" profile | Select the "(Package)" launch profile |
| Forgetting to reload extensions | Run `Reload` in Command Palette after deploying |
| CLSID mismatch | Ensure `[Guid]` in .cs matches `ClassId` in Package.appxmanifest (both places) |
| Logging in hot paths | `GetItems()` is called frequently — avoid expensive work or logging here |
@@ -0,0 +1,145 @@
---
name: add-adaptive-card-form
description: >-
Create form-based UI for your Command Palette extension using Adaptive Cards.
Use when asked to add forms, user input fields, toggle switches, text inputs,
dropdown menus, data entry, surveys, configuration dialogs, or interactive
content pages. Supports the Adaptive Cards Designer for visual form building.
---
# Add Forms with Adaptive Cards
Create interactive forms in your Command Palette extension using Adaptive Cards. Forms allow you to collect user input through text fields, toggles, dropdowns, and other controls.
## When to Use This Skill
- Adding a form to collect user input (name, settings, feedback)
- Creating interactive configuration dialogs
- Building data entry interfaces
- Adding toggle switches or dropdown menus
- Displaying complex layouts beyond simple lists
## Prerequisites
- Familiarity with [Adaptive Cards](https://adaptivecards.io/)
- Optional: Use the [Adaptive Card Designer](https://adaptivecards.io/designer/) to visually build your form
## Quick Start
### Step 1: Create a ContentPage with FormContent
Create a new file in your `Pages/` directory:
```csharp
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using System.Text.Json.Nodes;
namespace YourExtension;
internal sealed partial class MyFormPage : ContentPage
{
private readonly MyForm _form = new();
public MyFormPage()
{
Name = "Open";
Title = "My Form";
Icon = new IconInfo("\uECA5");
}
public override IContent[] GetContent() => [_form];
}
internal sealed partial class MyForm : FormContent
{
public MyForm()
{
TemplateJson = """
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "Input.Text",
"label": "Name",
"id": "Name",
"isRequired": true,
"errorMessage": "Name is required",
"placeholder": "Enter your name"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit"
}
]
}
""";
}
public override CommandResult SubmitForm(string payload)
{
var formInput = JsonNode.Parse(payload)?.AsObject();
if (formInput == null)
{
return CommandResult.GoHome();
}
var name = formInput["Name"]?.ToString() ?? "Unknown";
return CommandResult.ShowToast($"Hello, {name}!");
}
}
```
### Step 2: Register the Page
In your `CommandsProvider`, add the form page:
```csharp
_commands = [
new CommandItem(new MyFormPage()) { Title = "My Form" },
];
```
### Step 3: Deploy and Test
1. Deploy your extension
2. In Command Palette, run `Reload`
3. Navigate to your form and submit it
## Key Concepts
### TemplateJson
The JSON layout of your form (from Adaptive Cards schema). Design it at https://adaptivecards.io/designer/
### DataJson (Optional)
Dynamic data binding using `${...}` placeholders in your TemplateJson:
```csharp
TemplateJson = """{ "body": [{ "type": "TextBlock", "text": "${title}" }] }""";
DataJson = """{ "title": "Dynamic Title" }""";
```
### SubmitForm
Called when the user submits. Parse `payload` as JSON to read input values by their `id`.
### Mixing Content Types
You can combine forms with markdown on the same page:
```csharp
public override IContent[] GetContent() => [
new MarkdownContent("# Instructions\nFill out the form below."),
_form,
];
```
## Common Form Patterns
See [form-patterns.md](references/form-patterns.md) for template JSON for common form types.
## Documentation
- [Get user input with forms](https://learn.microsoft.com/windows/powertoys/command-palette/using-form-pages)
- [Adaptive Card Designer](https://adaptivecards.io/designer/)
- [Adaptive Cards Schema](https://adaptivecards.io/explorer/)
@@ -0,0 +1,536 @@
# Common Adaptive Card Form Patterns
Reusable template JSON and handler code for the most common form types in Command Palette extensions.
---
## Simple Text Input Form
A basic form with one or two text fields and a submit button.
### TemplateJson
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "Input.Text",
"id": "FirstName",
"label": "First Name",
"placeholder": "Enter your first name",
"isRequired": true,
"errorMessage": "First name is required"
},
{
"type": "Input.Text",
"id": "Email",
"label": "Email Address",
"placeholder": "user@example.com",
"style": "Email",
"isRequired": true,
"errorMessage": "A valid email is required"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit"
}
]
}
```
### SubmitForm Handler
```csharp
public override CommandResult SubmitForm(string payload)
{
var input = JsonNode.Parse(payload)?.AsObject();
if (input == null) return CommandResult.GoHome();
var firstName = input["FirstName"]?.ToString() ?? "";
var email = input["Email"]?.ToString() ?? "";
return CommandResult.ShowToast($"Registered {firstName} ({email})");
}
```
---
## Toggle/Checkbox Form
Use `Input.Toggle` for boolean on/off settings. Combine with `DataJson` for dynamic defaults.
### TemplateJson
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "TextBlock",
"text": "Preferences",
"weight": "Bolder",
"size": "Medium"
},
{
"type": "Input.Toggle",
"id": "AcceptsTerms",
"title": "I accept the terms and conditions",
"valueOn": "true",
"valueOff": "false",
"value": "false"
},
{
"type": "Input.Toggle",
"id": "EnableNotifications",
"title": "Enable notifications",
"valueOn": "true",
"valueOff": "false",
"value": "${notificationsDefault}"
},
{
"type": "Input.Toggle",
"id": "DarkMode",
"title": "Use dark mode",
"valueOn": "true",
"valueOff": "false",
"value": "${darkModeDefault}"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Save Preferences"
}
]
}
```
### DataJson (Dynamic Defaults)
```csharp
DataJson = """
{
"notificationsDefault": "true",
"darkModeDefault": "false"
}
""";
```
### SubmitForm Handler
```csharp
public override CommandResult SubmitForm(string payload)
{
var input = JsonNode.Parse(payload)?.AsObject();
if (input == null) return CommandResult.GoHome();
var accepted = input["AcceptsTerms"]?.ToString() == "true";
var notifications = input["EnableNotifications"]?.ToString() == "true";
var darkMode = input["DarkMode"]?.ToString() == "true";
if (!accepted)
{
return CommandResult.ShowToast("You must accept the terms to continue.");
}
// Save preferences...
return CommandResult.ShowToast("Preferences saved!");
}
```
---
## Choice Set (Dropdown/Radio) Form
Use `Input.ChoiceSet` for single-select dropdowns or radio buttons.
### Compact Style (Dropdown)
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "Input.ChoiceSet",
"id": "Priority",
"label": "Priority Level",
"style": "compact",
"value": "medium",
"choices": [
{ "title": "Low", "value": "low" },
{ "title": "Medium", "value": "medium" },
{ "title": "High", "value": "high" },
{ "title": "Critical", "value": "critical" }
]
},
{
"type": "Input.ChoiceSet",
"id": "Category",
"label": "Category",
"style": "compact",
"choices": [
{ "title": "Bug Report", "value": "bug" },
{ "title": "Feature Request", "value": "feature" },
{ "title": "Documentation", "value": "docs" },
{ "title": "Question", "value": "question" }
]
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Create Issue"
}
]
}
```
### Expanded Style (Radio Buttons)
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "Input.ChoiceSet",
"id": "Theme",
"label": "Select a theme",
"style": "expanded",
"value": "system",
"choices": [
{ "title": "Light", "value": "light" },
{ "title": "Dark", "value": "dark" },
{ "title": "System Default", "value": "system" }
]
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Apply"
}
]
}
```
---
## Multi-Section Form
Combine multiple input types with TextBlock headers to create organized, multi-section forms. Use `Action.ShowCard` for progressive disclosure of optional sections.
### TemplateJson
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "TextBlock",
"text": "Personal Information",
"weight": "Bolder",
"size": "Medium",
"separator": true
},
{
"type": "Input.Text",
"id": "FullName",
"label": "Full Name",
"placeholder": "Enter your full name",
"isRequired": true,
"errorMessage": "Name is required"
},
{
"type": "Input.Text",
"id": "Email",
"label": "Email",
"placeholder": "user@example.com",
"style": "Email"
},
{
"type": "TextBlock",
"text": "Preferences",
"weight": "Bolder",
"size": "Medium",
"separator": true,
"spacing": "Large"
},
{
"type": "Input.ChoiceSet",
"id": "Language",
"label": "Preferred Language",
"style": "compact",
"value": "en",
"choices": [
{ "title": "English", "value": "en" },
{ "title": "Spanish", "value": "es" },
{ "title": "French", "value": "fr" },
{ "title": "German", "value": "de" }
]
},
{
"type": "Input.Toggle",
"id": "Newsletter",
"title": "Subscribe to newsletter",
"valueOn": "true",
"valueOff": "false",
"value": "true"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Save Profile"
},
{
"type": "Action.ShowCard",
"title": "Advanced Options",
"card": {
"type": "AdaptiveCard",
"body": [
{
"type": "Input.Text",
"id": "ApiKey",
"label": "API Key (optional)",
"placeholder": "Enter your API key"
},
{
"type": "Input.Toggle",
"id": "DebugMode",
"title": "Enable debug mode",
"valueOn": "true",
"valueOff": "false",
"value": "false"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Save All"
}
]
}
}
]
}
```
---
## Feedback Form
A common pattern for collecting user feedback with a multiline text area and a rating.
### TemplateJson
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "TextBlock",
"text": "We'd love your feedback!",
"weight": "Bolder",
"size": "Medium"
},
{
"type": "TextBlock",
"text": "Tell us what you think and how we can improve.",
"wrap": true,
"spacing": "Small"
},
{
"type": "Input.ChoiceSet",
"id": "Rating",
"label": "How would you rate your experience?",
"style": "expanded",
"isRequired": true,
"errorMessage": "Please select a rating",
"choices": [
{ "title": "⭐ Poor", "value": "1" },
{ "title": "⭐⭐ Fair", "value": "2" },
{ "title": "⭐⭐⭐ Good", "value": "3" },
{ "title": "⭐⭐⭐⭐ Great", "value": "4" },
{ "title": "⭐⭐⭐⭐⭐ Excellent", "value": "5" }
]
},
{
"type": "Input.Text",
"id": "Comments",
"label": "Comments",
"placeholder": "Share your thoughts...",
"isMultiline": true,
"maxLength": 500
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Send Feedback"
}
]
}
```
### SubmitForm Handler with Confirmation Dialog
```csharp
public override CommandResult SubmitForm(string payload)
{
var input = JsonNode.Parse(payload)?.AsObject();
if (input == null) return CommandResult.GoHome();
var rating = input["Rating"]?.ToString() ?? "0";
var comments = input["Comments"]?.ToString() ?? "";
return CommandResult.Confirm(new ConfirmationArgs
{
Title = "Submit feedback?",
Description = $"Rating: {rating}/5\n\n{(string.IsNullOrEmpty(comments) ? "No comments" : comments)}",
PrimaryCommand = new AnonymousCommand(() =>
{
// Process and store feedback
new ToastStatusMessage("Thank you for your feedback!").Show();
})
{
Name = "Submit",
Result = CommandResult.Dismiss(),
},
});
}
```
---
## Tree Content with Forms (Comment/Reply Pattern)
Use `TreeContent` to create nested, threaded discussions where each node can contain a form for replies.
### Post Content (Tree Node)
```csharp
internal sealed partial class PostContent : TreeContent
{
private readonly string _author;
private readonly string _body;
private readonly PostReplyForm _replyForm;
private readonly List<PostContent> _childPosts = [];
public PostContent(string author, string body)
{
_author = author;
_body = body;
_replyForm = new PostReplyForm(this);
TemplateJson = """
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "TextBlock",
"text": "${author}",
"weight": "Bolder"
},
{
"type": "TextBlock",
"text": "${body}",
"wrap": true
}
]
}
""";
DataJson = $$"""{ "author": "{{_author}}", "body": "{{_body}}" }""";
}
public override IContent[] GetChildren() => [_replyForm, .. _childPosts];
public void AddReply(PostContent reply) => _childPosts.Add(reply);
}
```
### Reply Form (Child of Tree Node)
```csharp
internal sealed partial class PostReplyForm : FormContent
{
private readonly PostContent _parent;
public PostReplyForm(PostContent parent)
{
_parent = parent;
TemplateJson = """
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "Input.Text",
"id": "ReplyText",
"placeholder": "Write a reply...",
"isMultiline": true
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Reply"
}
]
}
""";
}
public override CommandResult SubmitForm(string payload)
{
var input = JsonNode.Parse(payload)?.AsObject();
if (input == null) return CommandResult.GoHome();
var replyText = input["ReplyText"]?.ToString();
if (!string.IsNullOrWhiteSpace(replyText))
{
_parent.AddReply(new PostContent("You", replyText));
}
return CommandResult.KeepOpen();
}
}
```
### Hosting the Thread on a ContentPage
```csharp
internal sealed partial class ThreadPage : ContentPage
{
private readonly PostContent _rootPost;
public ThreadPage()
{
Name = "Discussion";
Title = "Discussion Thread";
Icon = new IconInfo("\uE90A");
_rootPost = new PostContent("Alice", "Has anyone tried the new API?");
_rootPost.AddReply(new PostContent("Bob", "Yes! It works great."));
}
public override IContent[] GetContent() => [_rootPost];
}
```
+149
View File
@@ -0,0 +1,149 @@
---
name: add-dock-band
description: >-
Add dock band support to your Command Palette extension for persistent toolbar widgets.
Use when asked to add dock support, toolbar buttons, persistent UI widgets,
taskbar integration, live-updating status displays, quick-access buttons,
or always-visible controls. Supports single buttons, multi-button strips,
and live-updating content.
---
# Add Dock Band Support
The Command Palette Dock is a persistent toolbar at the edge of the user's screen. Your extension can provide **dock bands** — strips of items that appear in the Dock — giving users quick access to commands without opening the full Command Palette.
## When to Use This Skill
- Adding a quick-access button to the persistent toolbar
- Creating a multi-button toolbar strip
- Displaying live-updating information (clock, CPU usage, etc.)
- Providing frequently-used commands without opening the full palette
## Prerequisites
- Command Palette Extension SDK version 0.9 or later (`Microsoft.CommandPalette.Extensions` ≥ 0.9.260303001)
## Quick Start: Single Button Dock Band
Override `GetDockBands()` in your `CommandProvider`:
```csharp
public partial class MyCommandsProvider : CommandProvider
{
private readonly ICommandItem[] _commands;
private readonly ICommandItem _dockBand;
public MyCommandsProvider()
{
DisplayName = "My Extension";
Id = "com.mycompany.myextension"; // Unique ID required for dock
var mainPage = new MyPage();
_dockBand = new CommandItem(mainPage) { Title = DisplayName };
_commands = [new CommandItem(mainPage) { Title = DisplayName }];
}
public override ICommandItem[] TopLevelCommands() => _commands;
public override ICommandItem[]? GetDockBands() => [_dockBand];
}
```
## Multi-Button Dock Band
Use `WrappedDockItem` to create a band with multiple buttons:
```csharp
public override ICommandItem[]? GetDockBands()
{
var button1 = new ListItem(new OpenUrlCommand("https://github.com"))
{
Title = "GitHub",
Icon = new IconInfo("\uE774"),
};
var button2 = new ListItem(new OpenUrlCommand("https://learn.microsoft.com"))
{
Title = "Learn",
Icon = new IconInfo("\uE82D"),
};
var band = new WrappedDockItem(
[button1, button2],
"com.mycompany.myextension.quicklinks", // Unique band ID
"Quick Links");
return [band];
}
```
## Live-Updating Dock Band
Create a dock band that updates its content periodically (like a clock):
```csharp
internal sealed partial class LiveStatusBand : ListItem
{
private readonly System.Timers.Timer _timer;
public LiveStatusBand()
: base(new NoOpCommand() { Result = CommandResult.KeepOpen() })
{
Title = DateTime.Now.ToString("HH:mm");
Icon = new IconInfo("\uE823"); // Clock icon
_timer = new System.Timers.Timer(60_000); // Update every minute
_timer.Elapsed += (s, e) =>
{
Title = DateTime.Now.ToString("HH:mm");
Subtitle = DateTime.Now.ToString("dddd, MMMM d");
};
_timer.Start();
}
}
// In CommandProvider:
public override ICommandItem[]? GetDockBands()
{
var band = new WrappedDockItem(
[new LiveStatusBand()],
"com.mycompany.myextension.status",
"Live Status");
return [band];
}
```
## How Dock Bands Render
| Command Type on ICommandItem | Dock Behavior |
|------------------------------|---------------|
| `IInvokableCommand` | Single button that executes the command |
| `IListPage` | Each list item renders as a separate button in one band |
| `IContentPage` | Single expandable button with a flyout |
## Support Pinning Nested Commands
By default, only top-level commands and dock bands can be pinned. To allow pinning nested commands:
```csharp
public override ICommandItem? GetCommandItem(string id)
{
// Look up commands by their Id
foreach (var item in GetAllCommands())
{
if (item?.Command is ICommand cmd && cmd.Id == id)
return item;
}
return null;
}
```
## Important Notes
- All dock band `ICommandItem` objects must have a `Command` with a **non-empty `Id`** — items without an ID are ignored
- Set `Id` on your `CommandProvider` (e.g., `Id = "com.mycompany.myextension"`)
- Use `WrappedDockItem` for multi-button bands backed by a `ListPage`
- Keep dock band updates lightweight — they run frequently
## Documentation
- [Adding Dock support](https://learn.microsoft.com/windows/powertoys/command-palette/adding-dock-support)
@@ -0,0 +1,202 @@
---
name: add-extension-settings
description: >-
Add a settings page to your Command Palette extension.
Use when asked to add settings, preferences, configuration options,
toggles, text inputs, dropdowns, or user-customizable behavior.
Covers ToggleSetting, TextSetting, ChoiceSetSetting, and persistence.
---
# Add Extension Settings
Add a settings page to your Command Palette extension using the built-in settings helpers. Settings are automatically persisted and restored by the extension host.
## When to Use This Skill
- Adding user-configurable options to your extension
- Creating toggle switches for features
- Adding text input fields for configuration
- Creating dropdown menus for option selection
- Persisting user preferences across sessions
## Quick Start
### Step 1: Create a Settings Manager
Create a new file `SettingsManager.cs`:
```csharp
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace YourExtension;
internal sealed class SettingsManager
{
private readonly Settings _settings;
public SettingsManager()
{
_settings = new Settings();
var maxResults = new TextSetting(
"maxResults",
"Maximum Results",
"Maximum number of results to display",
"10");
var showSubtitles = new ToggleSetting(
"showSubtitles",
"Show Subtitles",
"Display subtitle text under each result",
true);
var sortOrder = new ChoiceSetSetting(
"sortOrder",
"Sort Order",
"How to sort results",
[
new ChoiceSetSetting.Choice("Alphabetical", "alpha"),
new ChoiceSetSetting.Choice("Most Recent", "recent"),
new ChoiceSetSetting.Choice("Most Used", "frequent"),
],
"alpha");
_settings.AddSetting(maxResults);
_settings.AddSetting(showSubtitles);
_settings.AddSetting(sortOrder);
// React to settings changes
_settings.SettingsChanged += OnSettingsChanged;
}
public ICommandSettings Settings => _settings;
public int MaxResults => int.TryParse(
_settings.GetSetting<string>("maxResults"), out var val) ? val : 10;
public bool ShowSubtitles =>
_settings.GetSetting<bool>("showSubtitles");
public string SortOrder =>
_settings.GetSetting<string>("sortOrder") ?? "alpha";
private void OnSettingsChanged(object? sender, EventArgs e)
{
// React to settings changes (e.g., refresh data)
}
}
```
### Step 2: Wire into CommandProvider
In your `CommandsProvider`, expose the settings:
```csharp
public partial class MyCommandsProvider : CommandProvider
{
private readonly SettingsManager _settingsManager = new();
private readonly ICommandItem[] _commands;
public MyCommandsProvider()
{
DisplayName = "My Extension";
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
Settings = _settingsManager.Settings; // This exposes settings to CmdPal
_commands = [
new CommandItem(new MyPage(_settingsManager)) { Title = DisplayName },
];
}
public override ICommandItem[] TopLevelCommands() => _commands;
}
```
### Step 3: Use Settings in Pages
```csharp
internal sealed partial class MyPage : ListPage
{
private readonly SettingsManager _settings;
public MyPage(SettingsManager settings)
{
_settings = settings;
}
public override IListItem[] GetItems()
{
var items = GetAllItems();
return items.Take(_settings.MaxResults).ToArray();
}
}
```
## Setting Types
| Type | UI Control | Value Type | Constructor Parameters |
|------|-----------|------------|----------------------|
| `ToggleSetting` | Toggle switch | `bool` | `(id, label, description, defaultValue)` |
| `TextSetting` | Text input | `string` | `(id, label, description, defaultValue)` |
| `ChoiceSetSetting` | Dropdown | `string` | `(id, label, description, choices[], defaultValue)` |
## Key Points
- Settings are automatically persisted by the CmdPal host
- Use `SettingsChanged` event to react to changes in real-time
- Access values via `GetSetting<T>(id)` with the setting's string id
- Pass the settings manager to pages/commands that need configuration
- Settings page appears automatically when `Settings` is set on `CommandProvider`
## Grouping Settings
For extensions with many settings, organize them into logical groups:
```csharp
public SettingsManager()
{
_settings = new Settings();
// Appearance group
var theme = new ChoiceSetSetting("theme", "Theme", "UI theme",
[
new ChoiceSetSetting.Choice("Light", "light"),
new ChoiceSetSetting.Choice("Dark", "dark"),
new ChoiceSetSetting.Choice("System", "system"),
],
"system");
var fontSize = new TextSetting("fontSize", "Font Size", "Display font size", "14");
// Behavior group
var autoRefresh = new ToggleSetting("autoRefresh", "Auto-Refresh",
"Automatically refresh results", true);
var refreshInterval = new TextSetting("refreshInterval", "Refresh Interval",
"Seconds between auto-refreshes", "30");
_settings.AddSetting(theme);
_settings.AddSetting(fontSize);
_settings.AddSetting(autoRefresh);
_settings.AddSetting(refreshInterval);
}
```
## Reacting to Changes
Use the `SettingsChanged` event to update behavior when the user modifies settings:
```csharp
private void OnSettingsChanged(object? sender, EventArgs e)
{
// Invalidate cached data
_cachedItems = null;
// Notify pages to refresh
OnItemsChanged?.Invoke(this, EventArgs.Empty);
}
```
## Documentation
- [SampleSettingsPage.cs](https://github.com/microsoft/PowerToys/blob/main/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleSettingsPage.cs)
@@ -0,0 +1,164 @@
---
name: add-fallback-commands
description: >-
Add fallback commands to your Command Palette extension for catch-all search behavior.
Use when asked to add search functionality, query matching, direct input handling,
calculator-style evaluation, URL opening, command execution, or results that appear
when no other extension matches. Used by 14 of 20 built-in extensions.
---
# Add Fallback Commands
Fallback commands are shown in Command Palette when no other results match the user's query. They enable your extension to act as a catch-all handler — perfect for calculators, web search, command execution, file path opening, and more.
## When to Use This Skill
- Adding search functionality that responds to any user input
- Creating a calculator that evaluates expressions as the user types
- Building a web search that triggers on unmatched queries
- Opening files or URLs typed directly into the palette
- Executing shell commands from the search bar
## How Fallback Commands Work
1. User types a query in Command Palette
2. If no top-level commands match, CmdPal asks extensions for fallback results
3. Your extension's `FallbackCommands()` provides items that respond to the query
4. The fallback items can be static (always shown) or dynamic (filtered by query)
## Quick Start: Static Fallback
Override `FallbackCommands()` in your `CommandProvider`:
```csharp
public partial class MyCommandsProvider : CommandProvider
{
private readonly ICommandItem[] _commands;
private readonly FallbackCommandItem[] _fallbacks;
public MyCommandsProvider()
{
DisplayName = "Web Search";
Icon = new IconInfo("\uE721"); // Search icon
var searchPage = new WebSearchPage();
_commands = [new CommandItem(searchPage) { Title = DisplayName }];
_fallbacks = [new FallbackCommandItem(searchPage) { Title = "Search the web" }];
}
public override ICommandItem[] TopLevelCommands() => _commands;
public override IFallbackCommandItem[] FallbackCommands() => _fallbacks;
}
```
## Dynamic Fallback with DynamicListPage
For fallbacks that filter results based on the query, use `DynamicListPage`:
```csharp
internal sealed partial class WebSearchPage : DynamicListPage
{
private string _query = string.Empty;
public WebSearchPage()
{
Icon = new IconInfo("\uE721");
Title = "Web Search";
Name = "Search";
PlaceholderText = "Type to search...";
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
_query = newSearch;
RaiseItemsChanged();
}
public override IListItem[] GetItems()
{
if (string.IsNullOrWhiteSpace(_query))
return [];
return [
new ListItem(new OpenUrlCommand($"https://www.google.com/search?q={Uri.EscapeDataString(_query)}"))
{
Title = $"Search Google for \"{_query}\"",
Icon = new IconInfo("\uE721"),
},
new ListItem(new OpenUrlCommand($"https://www.bing.com/search?q={Uri.EscapeDataString(_query)}"))
{
Title = $"Search Bing for \"{_query}\"",
Icon = new IconInfo("\uE721"),
},
];
}
}
```
## Responsive Fallback with Cancellation
For expensive operations (API calls, file searches), use cancellation to stay responsive:
```csharp
internal sealed partial class SmartSearchPage : DynamicListPage
{
private CancellationTokenSource? _cts;
private IListItem[] _results = [];
public override void UpdateSearchText(string oldSearch, string newSearch)
{
// Cancel any in-flight search
_cts?.Cancel();
_cts = new CancellationTokenSource();
var token = _cts.Token;
_ = Task.Run(async () =>
{
// Debounce: wait for user to stop typing
await Task.Delay(300, token);
if (token.IsCancellationRequested) return;
// Perform search
_results = await SearchAsync(newSearch, token);
RaiseItemsChanged();
}, token);
}
public override IListItem[] GetItems() => _results;
private async Task<IListItem[]> SearchAsync(string query, CancellationToken token)
{
// Your search logic here
// Check token.IsCancellationRequested periodically
return [];
}
}
```
## Real-World Examples (from built-in extensions)
| Extension | Fallback Behavior |
|-----------|------------------|
| **Apps** | Search installed applications by name |
| **Calc** | Evaluate mathematical expressions directly |
| **Shell** | Execute command-line commands |
| **WebSearch** | Search the web with configured engine |
| **Indexer** | Open files by path |
| **TimeDate** | Parse time/date queries |
| **WindowsSettings** | Jump to Windows Settings pages |
| **WinGet** | Search WinGet packages |
| **WindowWalker** | Find and switch to open windows |
## Key Points
- `FallbackCommands()` returns `IFallbackCommandItem[]` (not `ICommandItem[]`)
- Use `FallbackCommandItem` wrapper (not `CommandItem`)
- Wrap a `DynamicListPage` for query-reactive results
- Cancel previous searches when new input arrives
- Keep fallback responses fast — users expect instant results
- Use `PlaceholderText` on your page to guide users
## Documentation
- [Extension samples](https://learn.microsoft.com/windows/powertoys/command-palette/samples)
- [Extensibility overview](https://learn.microsoft.com/windows/powertoys/command-palette/extensibility-overview)
+66
View File
@@ -0,0 +1,66 @@
---
name: publish-extension
description: >-
Publish your Command Palette extension to the Microsoft Store or WinGet.
Use when asked to publish, distribute, release, deploy to store,
create MSIX packages, submit to WinGet, set up CI/CD for releases,
or automate builds with GitHub Actions.
---
# Publish Your Command Palette Extension
Guide for distributing your Command Palette extension through the Microsoft Store, WinGet, or both.
## When to Use This Skill
- Publishing your extension to the Microsoft Store
- Submitting your extension to WinGet for `winget install` discovery
- Setting up GitHub Actions to automate builds and releases
- Creating MSIX packages for Store submission
- Creating EXE installers for WinGet submission
## Publishing Options
| Channel | Package Format | Discovery | Auto-Updates |
|---------|---------------|-----------|--------------|
| Microsoft Store | MSIX bundle | Store app, `ms-windows-store://` link | Yes |
| WinGet | EXE installer | `winget install`, CmdPal browse | Yes (via manifest) |
**Recommendation**: Publish to both for maximum reach. WinGet enables direct discovery from within Command Palette.
## Workflows
### Microsoft Store Publishing
See [store-publishing.md](references/store-publishing.md) for the complete step-by-step guide.
**Summary:**
1. Register for Partner Center
2. Update `Package.appxmanifest` and `.csproj` with Partner Center identity
3. Build MSIX for x64 and ARM64
4. Create MSIX bundle
5. Submit to Partner Center
### WinGet Publishing
See [winget-publishing.md](references/winget-publishing.md) for the complete step-by-step guide.
**Summary:**
1. Switch project to unpackaged mode
2. Create Inno Setup installer script
3. Build EXE installers
4. Submit manifest via `wingetcreate new`
5. Optionally automate with GitHub Actions
## Prerequisites
- [Visual Studio](https://visualstudio.microsoft.com/) with C# and WinUI workloads
- [Partner Center account](https://partner.microsoft.com/dashboard/home) (for Store publishing)
- [GitHub CLI](https://cli.github.com/) (for WinGet publishing)
- [WingetCreate](https://github.com/microsoft/winget-create) — `winget install Microsoft.WingetCreate`
- [Inno Setup](https://jrsoftware.org/isdl.php) (for WinGet EXE packaging)
## Important Notes
- Your extension's CLSID (the `[Guid("...")]` in your main .cs file) must be unique and consistent across all files
- WinGet manifests must include the `windows-commandpalette-extension` tag for CmdPal discovery
- MSIX packages require both x64 and ARM64 builds for Store submission
- WindowsAppSdk must be listed as a dependency in WinGet manifests
@@ -0,0 +1,169 @@
# Microsoft Store Publishing Guide
Complete step-by-step guide for publishing your Command Palette extension to the Microsoft Store.
## Step 1: Set Up Microsoft Store
1. Go to [Partner Center](https://partner.microsoft.com/dashboard/home)
2. Navigate to **Apps and Games****New product****MSIX or PWA app**
3. Reserve your app name (e.g., `My Extension for Command Palette`)
4. Once created, go to **Product Management****Product Identity**
5. Copy these three values — you'll need them in the next step:
| Partner Center Field | Where It Goes |
|---------------------|---------------|
| **Package/Identity/Name** | `Package.appxmanifest``Identity Name` and `.csproj``AppxPackageIdentityName` |
| **Package/Identity/Publisher** | `Package.appxmanifest``Identity Publisher` and `.csproj``AppxPackagePublisher` |
| **Package/Properties/PublisherDisplayName** | `Package.appxmanifest``Properties PublisherDisplayName` |
## Step 2: Prepare the Extension
### Update `Package.appxmanifest`
Replace the placeholder identity values with your Partner Center values:
```xml
<Identity
Name="YOUR_PACKAGE_IDENTITY_NAME_HERE"
Publisher="YOUR_PACKAGE_IDENTITY_PUBLISHER_HERE"
Version="0.0.1.0" />
```
And update the publisher display name:
```xml
<Properties>
<DisplayName>Your Extension Name</DisplayName>
<PublisherDisplayName>YOUR_PUBLISHER_DISPLAY_NAME_HERE</PublisherDisplayName>
<!-- ... -->
</Properties>
```
### Update `.csproj`
Add or update the following properties in your `.csproj` file:
```xml
<PropertyGroup>
<AppxPackageIdentityName>YOUR_PACKAGE_IDENTITY_NAME_HERE</AppxPackageIdentityName>
<AppxPackagePublisher>YOUR_PACKAGE_IDENTITY_PUBLISHER_HERE</AppxPackagePublisher>
<AppxPackageVersion>0.0.1.0</AppxPackageVersion>
</PropertyGroup>
```
### Update Image Assets ItemGroup
Ensure all image assets are included in the package by updating the `ItemGroup`:
```xml
<ItemGroup>
<Content Include="Assets\**\*.png" />
</ItemGroup>
```
> **Tip:** The `Assets` folder should contain your Store logos and extension icons at the required sizes (44x44, 150x150, etc.). You can generate these from a single high-resolution image.
## Step 3: Build MSIX Packages
Build for both x64 and ARM64 architectures:
```powershell
# x64 build
dotnet build --configuration Release -p:GenerateAppxPackageOnBuild=true -p:Platform=x64 -p:AppxPackageDir="AppPackages\x64\"
# ARM64 build
dotnet build --configuration Release -p:GenerateAppxPackageOnBuild=true -p:Platform=ARM64 -p:AppxPackageDir="AppPackages\ARM64\"
```
Verify the MSIX files were created:
```powershell
dir AppPackages -Recurse -Filter "*.msix"
```
You should see two `.msix` files, one for each architecture.
## Step 4: Create MSIX Bundle
### Create the bundle mapping file
Create a file named `bundle_mapping.txt` that maps each MSIX to its architecture:
```text
[Files]
"AppPackages\x64\YourExtension_0.0.1.0_x64\YourExtension_0.0.1.0_x64.msix" "YourExtension_0.0.1.0_x64.msix"
"AppPackages\ARM64\YourExtension_0.0.1.0_ARM64\YourExtension_0.0.1.0_ARM64.msix" "YourExtension_0.0.1.0_ARM64.msix"
```
> **Note:** Update the paths and filenames to match your actual build output. Check the `AppPackages` directory structure after building.
### Run makeappx
```powershell
makeappx bundle /f bundle_mapping.txt /p YourExtension_0.0.1.0_Bundle.msixbundle
```
> **Tip:** `makeappx.exe` is included with the Windows SDK. If it's not in your PATH, find it at:
> `C:\Program Files (x86)\Windows Kits\10\bin\<version>\x64\makeappx.exe`
## Step 5: Submit to Partner Center
1. Go to [Partner Center](https://partner.microsoft.com/dashboard/home)
2. Navigate to your app → **Start a new submission**
3. In **Packages**, upload your `.msixbundle` file
4. In **Store Listings****Description**, include a note like:
> `YourExtension` integrates with the Windows Command Palette to provide [describe your extension's functionality]. Requires PowerToys with Command Palette enabled.
5. In **Notes for certification**, add testing instructions:
> This extension requires Microsoft PowerToys (available from the Microsoft Store or https://github.com/microsoft/PowerToys) with the Command Palette feature enabled. To test:
> 1. Install PowerToys and enable Command Palette
> 2. Install this extension
> 3. Open Command Palette (Win+Alt+Space by default)
> 4. Search for [your extension's commands]
6. Set **Availability** and pricing as appropriate
7. Click **Submit for certification**
Certification typically takes 13 business days.
## Validation Checklist
Before submitting, verify:
- [ ] Partner Center identity values match exactly in both `Package.appxmanifest` and `.csproj`
- [ ] `AppxPackageVersion` is set correctly and incremented from any previous submission
- [ ] Both x64 and ARM64 MSIX files are built successfully
- [ ] MSIX bundle is created without errors
- [ ] Extension installs and runs correctly from the MSIX package locally
- [ ] Store listing includes clear description mentioning Command Palette integration
- [ ] Testing instructions mention the PowerToys/Command Palette prerequisite
- [ ] All required Store logos and screenshots are provided
- [ ] Privacy policy URL is set (if your extension accesses network or user data)
## Store-Only Discovery Limitations
> **Important:** Command Palette cannot currently search for extensions published only to the Microsoft Store via its built-in browse experience. Users can find Store-published extensions through:
>
> - Direct Store link shared by the developer
> - The Store's extension tag URL:
> ```
> ms-windows-store://assoc/?Tags=AppExtension-com.microsoft.commandpalette
> ```
> - Searching the Store app directly
>
> For discoverability within Command Palette's browse experience, also publish to WinGet.
> See [winget-publishing.md](winget-publishing.md) for details.
## Updating Your Extension
To publish an update:
1. Increment the version in `.csproj` (`AppxPackageVersion`) and `Package.appxmanifest`
2. Rebuild MSIX packages for both architectures
3. Recreate the MSIX bundle with updated filenames
4. Create a new submission in Partner Center and upload the new bundle
5. Submit for certification
The Store will automatically update users who have installed your extension.
@@ -0,0 +1,413 @@
# WinGet Publishing Guide
Complete step-by-step guide for publishing your Command Palette extension to WinGet for `winget install` discovery and installation.
## Why WinGet?
Publishing to WinGet enables:
- Users to install via `winget install YourPublisher.YourExtension`
- Discovery directly inside Command Palette's built-in browse experience
- Automatic update detection via WinGet manifests
## Step 1: Prepare the Project for Unpackaged Distribution
WinGet distribution uses an unpackaged (EXE-based) build instead of MSIX.
### Update `.csproj`
Remove any existing `<PublishProfile>` property and add unpackaged mode:
```xml
<PropertyGroup>
<!-- Remove or comment out this line if present: -->
<!-- <PublishProfile>win-$(Platform)</PublishProfile> -->
<!-- Add this for unpackaged distribution: -->
<WindowsPackageType>None</WindowsPackageType>
</PropertyGroup>
```
### Note Your CLSID
Find the `[Guid("...")]` attribute in your main `.cs` file (e.g., `SampleExtension.cs`):
```csharp
[Guid("YOUR-GUID-HERE")]
public sealed partial class SampleExtension : IExtension
```
You'll need this exact GUID for the installer script. It must match across all files.
## Step 2: Create Installer Scripts
### Inno Setup Script: `setup-template.iss`
Create this file in your project root. Replace all `TODO` placeholders with your values:
```iss
; Inno Setup script for Command Palette extension
#define MyAppName "TODO_YOUR_EXTENSION_NAME"
#define MyAppVersion "TODO_YOUR_VERSION"
#define MyAppPublisher "TODO_YOUR_PUBLISHER_NAME"
#define MyAppURL "TODO_YOUR_PROJECT_URL"
#define MyAppCLSID "TODO_YOUR_CLSID_WITH_BRACES"
; Example CLSID: {12345678-1234-1234-1234-123456789012}
[Setup]
AppId={#MyAppCLSID}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
OutputBaseFilename={#MyAppName}_{#MyAppVersion}_{#SetupSetting("ArchitecturesAllowed")}
Compression=lzma
SolidCompression=yes
WizardStyle=modern
PrivilegesRequired=lowest
OutputDir=Installer
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Registry]
; Register the COM server for Command Palette discovery
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppName}"; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}\InprocServer32"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppName}.dll"; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}\InprocServer32"; ValueType: string; ValueName: "ThreadingModel"; ValueData: "Both"; Flags: uninsdeletekey
[UninstallDelete]
Type: filesandordirs; Name: "{app}"
```
> **Important:** The `AppId` must use your CLSID wrapped in braces. The registry entries register your extension's COM server so Command Palette can discover it.
### Build Script: `build-exe.ps1`
Create this PowerShell script in your project root:
```powershell
<#
.SYNOPSIS
Builds EXE installers for x64 and ARM64 using dotnet publish and Inno Setup.
.DESCRIPTION
Publishes the project for both architectures, then runs Inno Setup to create
EXE installers suitable for WinGet submission.
#>
param(
[string]$Configuration = "Release",
[string]$Version = "0.0.1"
)
$ErrorActionPreference = "Stop"
$projectName = (Get-ChildItem -Filter "*.csproj" | Select-Object -First 1).BaseName
if (-not $projectName) {
Write-Error "No .csproj file found in the current directory."
exit 1
}
$architectures = @("x64", "arm64")
foreach ($arch in $architectures) {
Write-Host "`n=== Building $arch ===" -ForegroundColor Cyan
# Publish
Write-Host "Publishing for $arch..."
dotnet publish -c $Configuration -r "win-$arch" -o "publish" --self-contained=false
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet publish failed for $arch"
exit 1
}
# Create installer
Write-Host "Creating installer for $arch..."
$issFile = "setup-template.iss"
if (-not (Test-Path $issFile)) {
Write-Error "Inno Setup script not found: $issFile"
exit 1
}
$archFlag = if ($arch -eq "arm64") { "arm64" } else { "x64" }
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
/DMyAppVersion="$Version" `
/DArchitecturesAllowed="$archFlag" `
$issFile
if ($LASTEXITCODE -ne 0) {
Write-Error "Inno Setup failed for $arch"
exit 1
}
# Clean publish directory for next architecture
Remove-Item -Recurse -Force "publish" -ErrorAction SilentlyContinue
Write-Host "=== $arch complete ===" -ForegroundColor Green
}
Write-Host "`nInstallers created in the 'Installer' directory:" -ForegroundColor Cyan
Get-ChildItem -Path "Installer" -Filter "*.exe" | ForEach-Object { Write-Host " $_" }
```
## Step 3: Build EXE Installers
Run the build script from your project directory:
```powershell
.\build-exe.ps1
```
This produces two EXE files in the `Installer` directory:
```
Installer\YourExtension_0.0.1_x64.exe
Installer\YourExtension_0.0.1_arm64.exe
```
Verify both installers work by running them locally and confirming your extension appears in Command Palette.
## Step 4: Create a GitHub Release
Tag your repository with the version and create a release with the EXE files:
```powershell
# Tag the release
git tag -a v0.0.1 -m "Release v0.0.1"
git push origin v0.0.1
# Create release and upload assets (requires GitHub CLI)
gh release create v0.0.1 `
"Installer\YourExtension_0.0.1_x64.exe" `
"Installer\YourExtension_0.0.1_arm64.exe" `
--title "v0.0.1" `
--notes "Initial release of YourExtension for Command Palette."
```
After creating the release, copy the download URLs for both EXE files — you'll need them for the WinGet submission.
## Step 5: Submit to WinGet
Use `wingetcreate` to generate a WinGet manifest and submit a pull request:
```powershell
wingetcreate new "<URL_TO_x64.exe>" "<URL_TO_arm64.exe>"
```
`wingetcreate` will interactively prompt you for:
| Prompt | Example Value |
|--------|---------------|
| **PackageIdentifier** | `YourPublisher.YourExtension` |
| **PackageVersion** | `0.0.1` |
| **PackageLocale** | `en-US` |
| **Publisher** | `Your Name` |
| **PackageName** | `YourExtension for Command Palette` |
| **License** | `MIT` |
| **ShortDescription** | `A Command Palette extension that does X` |
After answering all prompts, `wingetcreate` will create a PR against the [winget-pkgs](https://github.com/microsoft/winget-pkgs) repository.
## Step 6: Add the Command Palette Tag (CRITICAL)
> **This step is required for your extension to appear in Command Palette's browse experience.**
After `wingetcreate` generates the manifest files, you **must** edit each `.locale.*.yaml` file to add the Command Palette tag.
In every locale YAML file (e.g., `YourPublisher.YourExtension.locale.en-US.yaml`), add:
```yaml
Tags:
- windows-commandpalette-extension
```
Example of a complete locale file with the tag:
```yaml
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json
PackageIdentifier: YourPublisher.YourExtension
PackageVersion: 0.0.1
PackageLocale: en-US
Publisher: Your Name
PackageName: YourExtension for Command Palette
License: MIT
ShortDescription: A Command Palette extension that does X
Tags:
- windows-commandpalette-extension
ManifestType: defaultLocale
ManifestVersion: 1.6.0
```
Without this tag, Command Palette will not discover your extension in its browse experience.
## Step 7: Ensure WindowsAppSdk Dependency
Your WinGet manifest must declare a dependency on the Windows App SDK so it gets installed automatically. In the `installer.yaml` manifest file, add:
```yaml
Dependencies:
PackageDependencies:
- PackageIdentifier: Microsoft.WindowsAppRuntime.1.7
MinimumVersion: 7001.632.252.0
```
> **Note:** Update the version number to match the Windows App SDK version your project targets. Check your `.csproj` for the `WindowsAppSDK` package version.
## Step 8: GitHub Actions Automation (Optional)
Automate your build, release, and WinGet submission process with GitHub Actions.
### Release Workflow: `.github/workflows/release-extension.yml`
```yaml
name: Release Extension
on:
push:
tags:
- 'v*'
permissions:
contents: write
env:
PROJECT_NAME: YourExtension
DOTNET_VERSION: '9.0.x'
jobs:
build:
strategy:
matrix:
arch: [x64, arm64]
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install Inno Setup
run: choco install innosetup -y --no-progress
- name: Detect version
id: version
run: |
$tag = "${{ github.ref_name }}" -replace '^v', ''
echo "VERSION=$tag" >> $env:GITHUB_OUTPUT
- name: Publish
run: |
dotnet publish -c Release -r win-${{ matrix.arch }} -o publish --self-contained=false
- name: Create installer
run: |
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
/DMyAppVersion="${{ steps.version.outputs.VERSION }}" `
/DArchitecturesAllowed="${{ matrix.arch }}" `
setup-template.iss
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: installer-${{ matrix.arch }}
path: Installer/*.exe
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: artifacts/*.exe
generate_release_notes: true
winget-update:
needs: release
runs-on: windows-latest
steps:
- name: Detect version
id: version
run: |
$tag = "${{ github.ref_name }}" -replace '^v', ''
echo "VERSION=$tag" >> $env:GITHUB_OUTPUT
- name: Update WinGet manifest
run: |
$baseUrl = "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}"
wingetcreate update YourPublisher.YourExtension `
--version ${{ steps.version.outputs.VERSION }} `
--urls "$baseUrl/${{ env.PROJECT_NAME }}_${{ steps.version.outputs.VERSION }}_x64.exe" "$baseUrl/${{ env.PROJECT_NAME }}_${{ steps.version.outputs.VERSION }}_arm64.exe" `
--submit `
--token ${{ secrets.WINGET_PAT }}
```
### Required Secrets
| Secret | Description |
|--------|-------------|
| `WINGET_PAT` | GitHub Personal Access Token with `public_repo` scope, used by `wingetcreate` to submit PRs to `microsoft/winget-pkgs` |
### How It Works
1. **Push a version tag** (e.g., `git tag v0.0.2 && git push origin v0.0.2`)
2. **Build job** runs in parallel for x64 and ARM64, creating EXE installers
3. **Release job** creates a GitHub Release and uploads the EXE files
4. **WinGet update job** automatically submits an updated manifest to `winget-pkgs`
> **Note:** The `winget-update` job uses `wingetcreate update` (not `new`) because it assumes you've already submitted your initial manifest manually. For the first submission, follow Steps 57 above.
## Validation Checklist
Before submitting to WinGet, verify:
- [ ] `.csproj` has `<WindowsPackageType>None</WindowsPackageType>` set
- [ ] CLSID in `setup-template.iss` matches the `[Guid("...")]` in your main `.cs` file
- [ ] Both x64 and ARM64 EXE installers build successfully
- [ ] Installer registers the COM server correctly (check `HKCU\Software\Classes\CLSID\{your-clsid}`)
- [ ] Extension appears in Command Palette after installing via EXE
- [ ] Extension is removed from Command Palette after uninstalling
- [ ] GitHub Release contains both EXE files with correct download URLs
- [ ] WinGet manifest includes `windows-commandpalette-extension` tag
- [ ] WinGet manifest includes `WindowsAppRuntime` dependency
- [ ] `winget validate` passes on all manifest files
## Updating Your Extension on WinGet
For subsequent releases:
```powershell
wingetcreate update YourPublisher.YourExtension `
--version "0.0.2" `
--urls "<URL_TO_NEW_x64.exe>" "<URL_TO_NEW_arm64.exe>" `
--submit
```
Or simply push a new version tag if you've set up the GitHub Actions workflow above.
## Troubleshooting
| Issue | Solution |
|-------|----------|
| Extension not appearing in CmdPal browse | Verify the `windows-commandpalette-extension` tag is in your locale YAML |
| COM registration fails | Check that the CLSID matches exactly and registry paths are correct |
| `wingetcreate` validation errors | Run `winget validate --manifest <path>` and fix reported issues |
| Installer doesn't run silently | Add `/VERYSILENT /SUPPRESSMSGBOXES` flags for silent install support |
| Missing WindowsAppSdk at runtime | Ensure the `PackageDependencies` section is in your installer manifest |