--- 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(() => 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(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 |