10 KiB
description, applyTo
| description | applyTo |
|---|---|
| Comprehensive guide for developing Command Palette extensions — covers pages, content, commands, items, icons, settings, dock, and debugging | **/*.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:
[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.Commandsis currently supported - The
[Guid]must match the CLSID inPackage.appxmanifest
CommandProvider
Override TopLevelCommands() to register main commands. Optionally override FallbackCommands() and GetDockBands():
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:
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:
- COM server —
com:ComServerwith matching CLSID and-RegisterProcessAsComServerargs - App extension —
uap3:AppExtensionwithName="com.microsoft.commandpalette"andCreateInstance ClassIdmatching 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:
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:
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
Filtersproperty for category filtering - Call
RaiseItemsChanged()after updating items to notify the UI
ContentPage (Rich Content)
Displays rich content like markdown, forms, or images:
internal sealed partial class MyContentPage : ContentPage
{
public override IContent[] GetContent() => [
new MarkdownContent("# Hello\nThis is **markdown**."),
];
}
- Can return multiple
IContentitems (mix markdown, forms, images, etc.) - Supports
Commandsproperty for context menu items viaCommandContextItem
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:

FormContent (Adaptive Cards)
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
- Use
${...}placeholders inTemplateJsonbound toDataJsonproperties
Commands
InvokableCommand
Actions that do something when activated:
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
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:
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
// 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 theCommandProvider - Use
System.Timers.Timerfor periodic background updates
Status Messages and Toasts
// 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
- Select Debug configuration
- Deploy via Build > Deploy (not just Build) — this registers the MSIX package
- Press F5 to launch with debugger attached
- Use
Debug.Write()/Debug.WriteLine()for diagnostic output - Check Output window (Ctrl+Alt+O) set to "Debug"
- 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 |