Instruction file imported from dante32683/cmdpal-extension-suite (
.github/instructions/cmdpal-extension.instructions.md). Copyright stays with the author.
Project conventions: Read
AGENTS.mdat the repo root before using this file.AGENTS.mdcovers the branch workflow, build/deploy loop, log verification, and all established patterns for this specific codebase. This file is an SDK API reference only.
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 |