diff --git a/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md b/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md
index 131129bd2d..57b5c4bd42 100644
--- a/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md
+++ b/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md
@@ -1,7 +1,7 @@
---
author: Mike Griese
created on: 2024-07-19
-last updated: 2025-08-08
+last updated: 2026-02-05
issue id: n/a
---
@@ -75,6 +75,8 @@ functionality.
- [Advanced scenarios](#advanced-scenarios)
- [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 IV: Dock bands](#addenda-iv-dock-bands)
- [Class diagram](#class-diagram)
- [Future considerations](#future-considerations)
- [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments)
@@ -2045,6 +2047,87 @@ 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 IV: Dock bands
+
+The "dock" is another way to surface commands to the user. This is a
+toolbar-like window that can be docked to the side of the screen, or floated as
+its own window. It enables another surface for extensions to display real-time
+information and shortcuts to users.
+
+Bands are powered by the same interfaces as DevPal itself. Extensions can provide
+bands via the new `DockBand` property on `ICommandProvider3`.
+
+```csharp
+interface ICommandProvider3 requires ICommandProvider2
+{
+ ICommandItem[] GetDockBands();
+};
+```
+
+A **Dock Band** is one "strip of items" in the dock. Each band can have multiple
+items. This allows an extension to create a strip of buttons that should all be
+treated as a single unit. For example, a media player band will want probably
+four items:
+* one for the previous track
+* one for play/pause
+* one for next track
+* and one to display the album art and track title
+
+`GetDockBands` returns an array of `ICommandItem`s. Each `ICommandItem`
+represents one band in the dock. These represent all of the bands that an
+extension would allow the user to add to their dock.
+
+All of the `ICommandItem`s returned from `GetDockBands` **must** have a
+`Command` with a non-empty `Id` set. If the `Id` is null or empty, DevPal will
+ignore that band.
+
+Bands are not automatically added to the dock. Instead, the user must choose
+which bands they want to add. This is done via the DevPal settings page.
+Furthermore, bands are not displayed in the list of commands in DevPal itself.
+This allows extension authors to create objects that are only intended for the
+dock, without cluttering up the main DevPal UI, and vice versa.
+
+DevPal will then create UI in the dock for each band the user has chosen to add.
+What that looks like will depend on the `Command` in the `ICommandItem`:
+* A `IInvokableCommand` will be rendered as a single button. Think "the
+ time/date" button on the taskbar, that opens the notification center.
+* A `IListPage` will be rendered as a strip of buttons, one for each `IListItem`
+ in the list. Think "media controls" for a music player.
+* A `IContentPage` will be rendered as a single button. Clicking that button
+ will open a flyout with that content rendered in it. Think "weather" or "news"
+ flyouts.
+
+If the `Command` in the `IListItem`s of a band are pages, then clicking those
+buttons will open DevPal to that page, as if it were a flyout from the dock.
+
+The `.Title` property of the top-level `ICommandItem` representing the band will
+be used as the name of the band in the settings. So a media player band might
+want to set the `Title` to "Contoso Music Player", even if the individual
+buttons in the band don't show that title.
+
+Users may also "pin" a top-level command from DevPal into the dock. DevPal will
+take care of creating a new band (owned by devpal) with that command in it. This
+allows users to add quick shortcuts to their favorite commands in the dock.
+Think: pinning an app, or pinning a particular GitHub query.
+
+Bands are added via ID. An extension may choose to have a TopLevelCommand and a
+DockBand with the same `Id`. In this case, if the user pins the TopLevelCommand
+to the dock, DevPal will pin the band from `GetDockBands`, rather than creating
+a simple pinned command. This allows extension authors to seamlessly have a
+top-level command present a palette-specific experience, while also having a
+dock-specific experience. In our ongoing media player example, the top-level
+command might open DevPal to a full-featured music control page, while the dock
+band has simpler buttons on it (without a title/subtitle).
+
+Users may choose to have:
+* the orientation of the dock: vertical or horizontal
+* the size of the dock
+* which bands are shown in the dock
+* whether the "labels" (read: `Title` & `Subtitle`) of individual bands are
+ shown or hidden.
+ - Dock bands will still display the `Title` & `Subtitle` of each item in the
+ band as the tooltip on those items, even when the "labels" are hidden.
+
## 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.)
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs
index ca64c87b23..3ad4671263 100644
--- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs
@@ -6,7 +6,10 @@ using Windows.Foundation;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
-public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2
+public abstract partial class CommandProvider :
+ ICommandProvider,
+ ICommandProvider2,
+ ICommandProvider3
{
public virtual string Id { get; protected set; } = string.Empty;
@@ -48,6 +51,21 @@ public abstract partial class CommandProvider : ICommandProvider, ICommandProvid
}
}
+ ///
+ /// Get the dock bands provided by this command provider. Dock bands are
+ /// strips of items that appear on various UI surfaces in CmdPal, such as a
+ /// toolbar. Each ICommandItem returned from this method will be treated as
+ /// one atomic band by cmdpal.
+ ///
+ /// If the command on an item here is a
+ /// IListPage, then cmdpal will render all of the items on that page as one
+ /// band. You can use this to create complex bands with multiple buttons.
+ ///
+ public virtual ICommandItem[]? GetDockBands()
+ {
+ return null;
+ }
+
///
/// This is used to manually populate the WinRT type cache in CmdPal with
/// any interfaces that might not follow a straight linear path of requires.
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockItem.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockItem.cs
new file mode 100644
index 0000000000..2d1e25b7be
--- /dev/null
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockItem.cs
@@ -0,0 +1,68 @@
+// 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.
+
+namespace Microsoft.CommandPalette.Extensions.Toolkit;
+
+///
+/// Helper class for creating a band out of a set of items. This allows you to
+/// simply just instantiate a set of buttons as ListItems, then pass them in to
+/// this class to create a band from those items. For example:
+///
+/// ```cs
+/// var foo = new MyFooListItem();
+/// var bar = new MyBarListItem();
+/// var band = new WrappedDockItem([foo, bar], "com.me.myBand", "My cool desk band");
+/// ```
+///
+public partial class WrappedDockItem : CommandItem
+{
+ public override string Title => _itemTitle;
+
+ public override IIconInfo? Icon
+ {
+ get => _icon; set { _icon = value; }
+ }
+
+ public override ICommand? Command => _backingList;
+
+ private readonly string _itemTitle;
+ private readonly WrappedDockList _backingList;
+ private IIconInfo? _icon;
+
+ public IListItem[] Items { get => _backingList.GetItems(); set => _backingList.SetItems(value); }
+
+ public WrappedDockItem(
+ ICommand command,
+ string displayTitle)
+ {
+ _backingList = new WrappedDockList(command);
+ _itemTitle = string.IsNullOrEmpty(displayTitle) ? command.Name : displayTitle;
+ _icon = command.Icon;
+ }
+
+ // This was too much of a footgun - we'd internally create a ListItem that
+ // didn't bubble the prop change events back up. That was bad.
+ // public WrappedDockItem(
+ // ICommandItem item,
+ // string id,
+ // string displayTitle)
+ // {
+ // _backingList = new WrappedDockList(item, id);
+ // _itemTitle = string.IsNullOrEmpty(displayTitle) ? item.Title : displayTitle;
+ // _icon = item.Icon;
+ // }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Create a new dock band for a set of list items
+ ///
+ public WrappedDockItem(
+ IListItem[] items,
+ string id,
+ string displayTitle)
+ {
+ _backingList = new WrappedDockList(items, id, displayTitle);
+ _itemTitle = displayTitle;
+ }
+}
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockList.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockList.cs
new file mode 100644
index 0000000000..fbb30f0b17
--- /dev/null
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockList.cs
@@ -0,0 +1,86 @@
+// 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.
+
+namespace Microsoft.CommandPalette.Extensions.Toolkit;
+
+///
+/// Helper class for a list page that just holds a set of items as a band.
+/// The page itself doesn't do anything interesting.
+///
+internal sealed partial class WrappedDockList : ListPage
+{
+ private string _id;
+
+ public override string Id => _id;
+
+ private List _items;
+
+ internal WrappedDockList(ICommand command)
+ {
+ _items = new() { new ListItem(command) };
+ Name = command.Name;
+ _id = command.Id;
+ }
+
+ // Maybe revisit sometime.
+ // The hard problem is that the wrapping item will not
+ // listen for property changes on the inner item.
+ // public WrappedDockList(ICommandItem item, string id)
+ // {
+ // var command = item.Command;
+ // _items = new()
+ // {
+ // new ListItem(command)
+ // {
+ // Title = item.Title,
+ // Subtitle = item.Subtitle,
+ // Icon = item.Icon,
+ // MoreCommands = item.MoreCommands,
+ // },
+ // };
+ // Name = command.Name;
+ // _id = string.IsNullOrEmpty(id) ? command.Id : id;
+ // }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Create a new list page for the set of items provided.
+ ///
+ internal WrappedDockList(IListItem[] items, string id, string name)
+ {
+ _items = new(items);
+ Name = name;
+ _id = id;
+ }
+
+ internal WrappedDockList(ICommand[] items, string id, string name)
+ {
+ _items = new();
+ foreach (var item in items)
+ {
+ _items.Add(new ListItem(item));
+ }
+
+ Name = name;
+ _id = id;
+ }
+
+ public override IListItem[] GetItems()
+ {
+ return _items.ToArray();
+ }
+
+ internal void SetItems(IListItem[]? newItems)
+ {
+ if (newItems == null)
+ {
+ _items = [];
+ RaiseItemsChanged(0);
+ return;
+ }
+
+ ListHelpers.InPlaceUpdateList(_items, newItems);
+ RaiseItemsChanged(_items.Count);
+ }
+}
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl
index d7eed3bc15..dff98f6516 100644
--- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl
@@ -405,6 +405,11 @@ namespace Microsoft.CommandPalette.Extensions
{
Object[] GetApiExtensionStubs();
};
-
+ [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
+ interface ICommandProvider3 requires ICommandProvider2
+ {
+ ICommandItem[] GetDockBands();
+ };
+
}