diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IExtensionService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IExtensionService.cs
deleted file mode 100644
index dca49abe3a8a..000000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IExtensionService.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-// 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.
-
-using Windows.Foundation;
-
-namespace Microsoft.CmdPal.Common.Services;
-
-public interface IExtensionService
-{
- ///
- /// Gets the currently cached installed Command Palette extensions.
- ///
- /// True to include disabled extensions in the result.
- /// A sequence of installed Command Palette extensions from the current in-memory cache.
- Task> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false);
-
- ///
- /// Forces a fresh scan of installed Command Palette extensions and updates the in-memory cache.
- ///
- /// True to include disabled extensions in the result.
- /// A sequence of installed Command Palette extensions after the cache has been rebuilt.
- Task> RefreshInstalledExtensionsAsync(bool includeDisabledExtensions = false);
-
- // Task> GetInstalledHomeWidgetPackageFamilyNamesAsync(bool includeDisabledExtensions = false);
- ///
- /// Gets the installed Command Palette extensions for a specific provider type.
- ///
- /// The provider type to match.
- /// True to include disabled extensions in the result.
- /// A sequence of installed Command Palette extensions for the requested provider type.
- Task> GetInstalledExtensionsAsync(Microsoft.CommandPalette.Extensions.ProviderType providerType, bool includeDisabledExtensions = false);
-
- ///
- /// Gets a cached installed extension by its unique id.
- ///
- /// The unique id of the extension to look up.
- /// The cached extension if found; otherwise, null.
- IExtensionWrapper? GetInstalledExtension(string extensionUniqueId);
-
- ///
- /// Signals running extensions to stop.
- ///
- Task SignalStopExtensionsAsync();
-
- ///
- /// Raised when one or more extensions are added to the installed set.
- ///
- event TypedEventHandler>? OnExtensionAdded;
-
- ///
- /// Raised when one or more extensions are removed from the installed set.
- ///
- event TypedEventHandler>? OnExtensionRemoved;
-
- ///
- /// Enables an installed extension by unique id.
- ///
- /// The unique id of the extension to enable.
- void EnableExtension(string extensionUniqueId);
-
- ///
- /// Disables an installed extension by unique id.
- ///
- /// The unique id of the extension to disable.
- void DisableExtension(string extensionUniqueId);
-
- /////
- ///// Gets a boolean indicating whether the extension was disabled due to the corresponding Windows optional feature
- ///// being absent from the machine or in an unknown state.
- /////
- ///// The out of proc extension object
- ///// True only if the extension was disabled. False otherwise.
- // public Task DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension);
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs
index 7db1a52ffe29..889a031d59fa 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs
@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using Microsoft.CmdPal.UI.ViewModels.Commands;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -18,8 +19,7 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
private readonly FallbackReloadItem _fallbackReloadItem = new();
private readonly FallbackLogItem _fallbackLogItem = new();
private readonly NewExtensionPage _newExtension = new();
-
- private readonly IRootPageService _rootPageService;
+ private readonly GoHomeDockCommand _goHomeDockCommand = new();
public override ICommandItem[] TopLevelCommands() =>
[
@@ -41,22 +41,16 @@ public override IFallbackCommandItem[] FallbackCommands() =>
_fallbackLogItem,
];
- public BuiltInsCommandProvider(IRootPageService rootPageService)
+ public BuiltInsCommandProvider()
{
Id = "com.microsoft.cmdpal.builtin.core";
DisplayName = Properties.Resources.builtin_display_name;
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
-
- _rootPageService = rootPageService;
}
public override ICommandItem[]? GetDockBands()
{
- var rootPage = _rootPageService.GetRootPage();
- List bandItems = new();
- bandItems.Add(new WrappedDockItem(rootPage, Properties.Resources.builtin_command_palette_title));
-
- return bandItems.ToArray();
+ return [new WrappedDockItem(_goHomeDockCommand, Properties.Resources.builtin_command_palette_title)];
}
public override void InitializeWithHost(IExtensionHost host) => BuiltinsExtensionHost.Instance.Initialize(host);
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/GoHomeDockCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/GoHomeDockCommand.cs
new file mode 100644
index 000000000000..5f264ca16725
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/GoHomeDockCommand.cs
@@ -0,0 +1,23 @@
+// 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.
+
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.UI.ViewModels.Commands;
+
+///
+/// A lightweight command used as a dock band item that navigates the user
+/// back to the Command Palette home page when invoked.
+///
+internal sealed partial class GoHomeDockCommand : InvokableCommand
+{
+ public GoHomeDockCommand()
+ {
+ Name = Properties.Resources.builtin_command_palette_title;
+ Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
+ }
+
+ public override ICommandResult Invoke() => CommandResult.GoHome();
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Gallery/ExtensionGalleryViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Gallery/ExtensionGalleryViewModel.cs
index a8428ae1421e..3611fe6aa2bb 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Gallery/ExtensionGalleryViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Gallery/ExtensionGalleryViewModel.cs
@@ -13,6 +13,7 @@
using Microsoft.CmdPal.Common.WinGet.Models;
using Microsoft.CmdPal.Common.WinGet.Services;
using Microsoft.CmdPal.UI.ViewModels.Properties;
+using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.Logging;
@@ -50,7 +51,7 @@ private static readonly CompositeFormat LabelGalleryExtensionsFound
"Failed to check WinGet package status");
private readonly IExtensionGalleryService _galleryService;
- private readonly IExtensionService _extensionService;
+ private readonly IEnumerable _extensionServices;
private readonly ILogger _logger;
private readonly ExtensionGalleryItemViewModelFactory _galleryExtensionViewModelFactory;
private readonly IWinGetPackageManagerService? _winGetPackageManagerService;
@@ -162,7 +163,7 @@ public string ItemCounterText
public ExtensionGalleryViewModel(
IExtensionGalleryService galleryService,
- IExtensionService extensionService,
+ IEnumerable extensionServices,
ILogger logger,
ExtensionGalleryItemViewModelFactory galleryExtensionViewModelFactory,
IWinGetPackageManagerService? winGetPackageManagerService = null,
@@ -171,7 +172,7 @@ public ExtensionGalleryViewModel(
TaskScheduler? uiScheduler = null)
{
_galleryService = galleryService;
- _extensionService = extensionService;
+ _extensionServices = extensionServices;
_logger = logger;
_galleryExtensionViewModelFactory = galleryExtensionViewModelFactory;
_winGetPackageManagerService = winGetPackageManagerService;
@@ -347,17 +348,23 @@ private async Task CheckInstalledAsync(
List snapshot;
try
{
- var installedExtensions = refreshInstalledExtensions
- ? await RunInBackgroundAsync(
- () => _extensionService.RefreshInstalledExtensionsAsync(includeDisabledExtensions: true),
- cancellationToken)
- : await RunInBackgroundAsync(
- () => _extensionService.GetInstalledExtensionsAsync(includeDisabledExtensions: true),
- cancellationToken);
+ var allInstalledExtensions = new List();
+ foreach (var service in _extensionServices)
+ {
+ var extensions = refreshInstalledExtensions
+ ? await RunInBackgroundAsync(
+ () => service.RefreshInstalledExtensionsAsync(includeDisabledExtensions: true),
+ cancellationToken)
+ : await RunInBackgroundAsync(
+ () => service.GetInstalledExtensionsAsync(includeDisabledExtensions: true),
+ cancellationToken);
+ allInstalledExtensions.AddRange(extensions);
+ }
+
cancellationToken.ThrowIfCancellationRequested();
var installedPfns = new HashSet(
- installedExtensions
+ allInstalledExtensions
.Select(e => e.PackageFamilyName)
.Where(pfn => !string.IsNullOrEmpty(pfn)),
StringComparer.OrdinalIgnoreCase);
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ProviderEnabledStateChangedMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ProviderEnabledStateChangedMessage.cs
new file mode 100644
index 000000000000..f3da124d5595
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ProviderEnabledStateChangedMessage.cs
@@ -0,0 +1,7 @@
+// 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.CmdPal.UI.ViewModels.Messages;
+
+public record ProviderEnabledStateChangedMessage(string ProviderId, bool IsEnabled);
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs
index 35fc866c3701..e6338b5646eb 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs
@@ -120,7 +120,7 @@ public bool IsEnabled
ProviderSettings = s.ProviderSettings.SetItem(_provider.ProviderId, newSettings),
});
_providerSettings = newSettings;
- WeakReferenceMessenger.Default.Send(new());
+ WeakReferenceMessenger.Default.Send(new(_provider.ProviderId, value));
OnPropertyChanged(nameof(IsEnabled));
OnPropertyChanged(nameof(ExtensionSubtext));
OnPropertyChanged(nameof(Icon));
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/BuiltInExtensionService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/BuiltInExtensionService.cs
new file mode 100644
index 000000000000..599e42ab769b
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/BuiltInExtensionService.cs
@@ -0,0 +1,80 @@
+// 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.
+
+using Microsoft.CmdPal.Common.Services;
+using Microsoft.CommandPalette.Extensions;
+using Windows.Foundation;
+
+namespace Microsoft.CmdPal.UI.ViewModels.Services;
+
+///
+/// Extension service that manages in-process built-in command providers
+/// registered in the DI container as .
+///
+public sealed class BuiltInExtensionService : IExtensionService
+{
+ private readonly IEnumerable _commandProviders;
+ private readonly TaskScheduler _taskScheduler;
+ private readonly List _wrappers = [];
+
+#pragma warning disable CS0067 // Events are required by the interface but not raised by this implementation
+ public event TypedEventHandler>? OnProviderAdded;
+
+ public event TypedEventHandler>? OnProviderRemoved;
+#pragma warning restore CS0067
+
+ public BuiltInExtensionService(IEnumerable commandProviders, TaskScheduler taskScheduler)
+ {
+ _commandProviders = commandProviders;
+ _taskScheduler = taskScheduler;
+ }
+
+ public Task> LoadProvidersAsync(CancellationToken ct)
+ {
+ if (ct.IsCancellationRequested)
+ {
+ return Task.FromResult>([]);
+ }
+
+ var wrappers = new List();
+ foreach (var provider in _commandProviders)
+ {
+ wrappers.Add(new CommandProviderWrapper(provider, _taskScheduler));
+ }
+
+ return Task.FromResult>(wrappers);
+ }
+
+ public Task SignalStopAsync()
+ {
+ // Built-in providers are in-proc and don't need explicit stop signaling.
+ return Task.CompletedTask;
+ }
+
+ public Task> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false)
+ {
+ return Task.FromResult>(_wrappers);
+ }
+
+ public Task> RefreshInstalledExtensionsAsync(bool includeDisabledExtensions = false)
+ {
+ // Built-in set is fixed at startup; refresh is a no-op.
+ return GetInstalledExtensionsAsync(includeDisabledExtensions);
+ }
+
+ public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId)
+ {
+ return _wrappers.FirstOrDefault(w => w.ExtensionUniqueId == extensionUniqueId);
+ }
+
+ public void EnableExtension(string extensionUniqueId)
+ {
+ // Nothing to do here. We're built-in extensions.
+ }
+
+ public void DisableExtension(string extensionUniqueId)
+ {
+ // Nothing to do here. We're built-in extensions.
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ExtensionStartResult.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ExtensionStartResult.cs
new file mode 100644
index 000000000000..f4b2944325a8
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ExtensionStartResult.cs
@@ -0,0 +1,48 @@
+// 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.
+
+using System.Diagnostics;
+using Microsoft.CmdPal.Common.Services;
+
+namespace Microsoft.CmdPal.UI.ViewModels.Services;
+
+///
+/// Represents the outcome of attempting to start a single WinRT extension.
+///
+internal sealed class ExtensionStartResult
+{
+ public IExtensionWrapper Extension { get; }
+
+ public CommandProviderWrapper? Wrapper { get; private init; }
+
+ public Task? PendingStartTask { get; private init; }
+
+ public Stopwatch? Stopwatch { get; private init; }
+
+ [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(Wrapper))]
+ public bool IsStarted => Wrapper is not null;
+
+ [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(PendingStartTask), nameof(Stopwatch))]
+ public bool IsTimedOut => PendingStartTask is not null;
+
+ private ExtensionStartResult(IExtensionWrapper extension)
+ {
+ Extension = extension;
+ }
+
+ public static ExtensionStartResult Started(IExtensionWrapper extension, CommandProviderWrapper wrapper)
+ {
+ return new ExtensionStartResult(extension) { Wrapper = wrapper };
+ }
+
+ public static ExtensionStartResult TimedOut(IExtensionWrapper extension, Task pendingStartTask, Stopwatch sw)
+ {
+ return new ExtensionStartResult(extension) { PendingStartTask = pendingStartTask, Stopwatch = sw };
+ }
+
+ public static ExtensionStartResult Failed(IExtensionWrapper extension)
+ {
+ return new ExtensionStartResult(extension);
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/IExtensionService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/IExtensionService.cs
new file mode 100644
index 000000000000..173024671ea0
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/IExtensionService.cs
@@ -0,0 +1,67 @@
+// 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.
+
+using Microsoft.CmdPal.Common.Services;
+using Windows.Foundation;
+
+namespace Microsoft.CmdPal.UI.ViewModels.Services;
+
+public interface IExtensionService
+{
+ ///
+ /// Loads command providers managed by this service. Returns providers that
+ /// are immediately ready. Slow or late providers arrive via .
+ ///
+ /// Cancellation token owned by the caller to cancel in-flight loading.
+ /// Command provider wrappers that are started and ready for command loading.
+ Task> LoadProvidersAsync(CancellationToken ct);
+
+ ///
+ /// Signals running providers managed by this service to stop/dispose.
+ ///
+ Task SignalStopAsync();
+
+ ///
+ /// Gets the currently cached installed extensions managed by this service.
+ ///
+ /// True to include disabled extensions in the result.
+ /// A sequence of installed extensions from the current in-memory cache.
+ Task> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false);
+
+ ///
+ /// Forces a fresh scan of installed extensions and updates the in-memory cache.
+ ///
+ /// True to include disabled extensions in the result.
+ /// A sequence of installed extensions after the cache has been rebuilt.
+ Task> RefreshInstalledExtensionsAsync(bool includeDisabledExtensions = false);
+
+ ///
+ /// Gets a cached installed extension by its unique id.
+ ///
+ /// The unique id of the extension to look up.
+ /// The cached extension if found; otherwise, null.
+ IExtensionWrapper? GetInstalledExtension(string extensionUniqueId);
+
+ ///
+ /// Enables an installed extension by unique id.
+ ///
+ /// The unique id of the extension to enable.
+ void EnableExtension(string extensionUniqueId);
+
+ ///
+ /// Disables an installed extension by unique id.
+ ///
+ /// The unique id of the extension to disable.
+ void DisableExtension(string extensionUniqueId);
+
+ ///
+ /// Raised when one or more providers become available (late start, new package install, etc.).
+ ///
+ event TypedEventHandler>? OnProviderAdded;
+
+ ///
+ /// Raised when one or more providers are removed (package uninstall, etc.).
+ ///
+ event TypedEventHandler>? OnProviderRemoved;
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/WinRTExtensionService.cs
similarity index 69%
rename from src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs
rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/WinRTExtensionService.cs
index 1e791fa3ca0e..13ef75090d7b 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/WinRTExtensionService.cs
@@ -2,28 +2,34 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Immutable;
+using System.Diagnostics;
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
+using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppExtensions;
using Windows.Foundation;
using Windows.Foundation.Collections;
-namespace Microsoft.CmdPal.UI.ViewModels.Models;
+namespace Microsoft.CmdPal.UI.ViewModels.Services;
-public partial class ExtensionService : IExtensionService, IDisposable
+///
+/// Extension service that manages out-of-process WinRT AppExtension-based command providers.
+/// Handles package catalog monitoring, extension startup with timeouts, and background retries.
+///
+public partial class WinRTExtensionService : IExtensionService, IDisposable
{
- public event TypedEventHandler>? OnExtensionAdded;
-
- public event TypedEventHandler>? OnExtensionRemoved;
+ private static readonly TimeSpan ExtensionStartTimeout = TimeSpan.FromSeconds(10);
+ private static readonly TimeSpan BackgroundStartTimeout = TimeSpan.FromSeconds(60);
private static readonly PackageCatalog _catalog = PackageCatalog.OpenForCurrentUser();
private static readonly Lock _lock = new();
private readonly SemaphoreSlim _getInstalledExtensionsLock = new(1, 1);
- private readonly SemaphoreSlim _getInstalledWidgetsLock = new(1, 1);
+ private readonly TaskScheduler _taskScheduler;
+ private readonly ICommandProviderCache _commandProviderCache;
- // private readonly ILocalSettingsService _localSettingsService;
private bool _disposedValue;
private const string CreateInstanceProperty = "CreateInstance";
@@ -32,17 +38,119 @@ public partial class ExtensionService : IExtensionService, IDisposable
private static readonly List _installedExtensions = [];
private static readonly List _enabledExtensions = [];
- public ExtensionService()
+ public event TypedEventHandler>? OnProviderAdded;
+
+ public event TypedEventHandler>? OnProviderRemoved;
+
+ public WinRTExtensionService(TaskScheduler taskScheduler, ICommandProviderCache commandProviderCache)
{
+ _taskScheduler = taskScheduler;
+ _commandProviderCache = commandProviderCache;
+
_catalog.PackageInstalling += Catalog_PackageInstalling;
_catalog.PackageUninstalling += Catalog_PackageUninstalling;
_catalog.PackageUpdating += Catalog_PackageUpdating;
+ }
+
+ public async Task> LoadProvidersAsync(CancellationToken ct)
+ {
+ var extensions = (await GetInstalledExtensionsAsync().ConfigureAwait(false)).ToImmutableList();
+
+ var timer = Stopwatch.StartNew();
- //// These two were an investigation into getting updates when a package
- //// gets redeployed from VS. Neither get raised (nor do the above)
- //// _catalog.PackageStatusChanged += Catalog_PackageStatusChanged;
- //// _catalog.PackageStaging += Catalog_PackageStaging;
- // _localSettingsService = settingsService;
+ // Start all extensions in parallel
+ var startResults = await Task.WhenAll(extensions.Select(ext => TryStartExtensionAsync(ext, ct))).ConfigureAwait(false);
+
+ var startedWrappers = new List();
+ foreach (var r in startResults)
+ {
+ if (r.IsStarted)
+ {
+ startedWrappers.Add(r.Wrapper);
+ }
+ else if (r.IsTimedOut)
+ {
+ _ = StartExtensionWhenReadyAsync(r.Extension, r.PendingStartTask, r.Stopwatch, ct);
+ }
+ }
+
+ timer.Stop();
+ Logger.LogInfo($"WinRTExtensionService: Started {startedWrappers.Count} extension(s) in {timer.ElapsedMilliseconds} ms");
+
+ return startedWrappers;
+ }
+
+ public async Task SignalStopAsync()
+ {
+ var installedExtensions = await GetInstalledExtensionsAsync().ConfigureAwait(false);
+ foreach (var installedExtension in installedExtensions)
+ {
+ Logger.LogDebug($"Signaling dispose to {installedExtension.ExtensionUniqueId}");
+ try
+ {
+ if (installedExtension.IsRunning())
+ {
+ installedExtension.SignalDispose();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to send dispose signal to extension {installedExtension.ExtensionUniqueId}", ex);
+ }
+ }
+ }
+
+ private async Task TryStartExtensionAsync(IExtensionWrapper extension, CancellationToken ct)
+ {
+ Logger.LogDebug($"Starting {extension.PackageFullName}");
+ var sw = Stopwatch.StartNew();
+ var startTask = extension.StartExtensionAsync();
+ try
+ {
+ await startTask.WaitAsync(ExtensionStartTimeout, ct).ConfigureAwait(false);
+ Logger.LogInfo($"Started extension {extension.PackageFullName} in {sw.ElapsedMilliseconds} ms");
+ return ExtensionStartResult.Started(extension, new CommandProviderWrapper(extension, _taskScheduler, _commandProviderCache));
+ }
+ catch (TimeoutException)
+ {
+ Logger.LogWarning($"Starting extension {extension.PackageFullName} timed out after {sw.ElapsedMilliseconds} ms, continuing in background");
+ return ExtensionStartResult.TimedOut(extension, startTask, sw);
+ }
+ catch (OperationCanceledException)
+ {
+ Logger.LogDebug($"Starting extension {extension.PackageFullName} was cancelled after {sw.ElapsedMilliseconds} ms");
+ return ExtensionStartResult.Failed(extension);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to start extension {extension.PackageFullName} after {sw.ElapsedMilliseconds} ms: {ex}");
+ return ExtensionStartResult.Failed(extension);
+ }
+ }
+
+ private async Task StartExtensionWhenReadyAsync(
+ IExtensionWrapper extension,
+ Task startTask,
+ Stopwatch sw,
+ CancellationToken ct)
+ {
+ try
+ {
+ await startTask.WaitAsync(BackgroundStartTimeout, ct).ConfigureAwait(false);
+
+ var wrapper = new CommandProviderWrapper(extension, _taskScheduler, _commandProviderCache);
+ Logger.LogInfo($"Late-started extension {extension.PackageFullName} in {sw.ElapsedMilliseconds} ms");
+
+ OnProviderAdded?.Invoke(this, [wrapper]);
+ }
+ catch (OperationCanceledException)
+ {
+ // Reload happened -- discard stale results
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Background start of extension {extension.PackageFullName} failed after {sw.ElapsedMilliseconds} ms: {ex}");
+ }
}
private void Catalog_PackageInstalling(PackageCatalog sender, PackageInstallingEventArgs args)
@@ -73,10 +181,7 @@ private void Catalog_PackageUpdating(PackageCatalog sender, PackageUpdatingEvent
{
lock (_lock)
{
- // Get any extension providers that we previously had from this app
UninstallPackageUnderLock(args.TargetPackage);
-
- // then add the new ones.
InstallPackageUnderLock(args.TargetPackage);
}
}
@@ -103,7 +208,25 @@ private void InstallPackageUnderLock(Package package)
UpdateExtensionsListsFromWrappers(wrappers);
- OnExtensionAdded?.Invoke(this, wrappers);
+ // Start extensions and notify via OnProviderAdded
+ var startedProviders = new List();
+ foreach (var wrapper in wrappers)
+ {
+ try
+ {
+ await wrapper.StartExtensionAsync().ConfigureAwait(false);
+ startedProviders.Add(new CommandProviderWrapper(wrapper, _taskScheduler, _commandProviderCache));
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to start newly installed extension {wrapper.ExtensionUniqueId}: {ex}");
+ }
+ }
+
+ if (startedProviders.Count > 0)
+ {
+ OnProviderAdded?.Invoke(this, startedProviders);
+ }
}
finally
{
@@ -121,7 +244,6 @@ private void UninstallPackageUnderLock(Package package)
if (extension.PackageFullName == package.Id.FullName)
{
CommandPaletteHost.Instance.DebugLog($"Uninstalled extension app {extension.PackageDisplayName}");
-
removedExtensions.Add(extension);
}
}
@@ -134,7 +256,24 @@ private void UninstallPackageUnderLock(Package package)
_installedExtensions.RemoveAll(i => removedExtensions.Contains(i));
_enabledExtensions.RemoveAll(i => removedExtensions.Contains(i));
- OnExtensionRemoved?.Invoke(this, removedExtensions);
+ // Build placeholder wrappers for removal notification.
+ // The TopLevelCommandManager matches by Extension reference, so we need to pass
+ // something that carries the IExtensionWrapper identity.
+ var removedProviders = new List();
+ foreach (var ext in removedExtensions)
+ {
+ try
+ {
+ removedProviders.Add(new CommandProviderWrapper(ext, _taskScheduler, _commandProviderCache));
+ }
+ catch
+ {
+ // Extension may not be in a runnable state if it was uninstalled;
+ // we still need to signal removal
+ }
+ }
+
+ OnProviderRemoved?.Invoke(this, removedProviders);
}
finally
{
@@ -143,52 +282,6 @@ private void UninstallPackageUnderLock(Package package)
});
}
- private static async Task IsValidCmdPalExtension(Package package)
- {
- var extensions = await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync();
- foreach (var extension in extensions)
- {
- if (package.Id?.FullName == extension.Package?.Id?.FullName)
- {
- var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
-
- return new(cmdPalProvider is not null && classId.Count != 0, extension);
- }
- }
-
- return new(false, null);
- }
-
- private static async Task<(IPropertySet? CmdPalProvider, List ClassIds)> GetCmdPalExtensionPropertiesAsync(AppExtension extension)
- {
- var classIds = new List();
- var properties = await extension.GetExtensionPropertiesAsync();
-
- if (properties is null)
- {
- return (null, classIds);
- }
-
- var cmdPalProvider = GetSubPropertySet(properties, "CmdPalProvider");
- if (cmdPalProvider is null)
- {
- return (null, classIds);
- }
-
- var activation = GetSubPropertySet(cmdPalProvider, "Activation");
- if (activation is null)
- {
- return (cmdPalProvider, classIds);
- }
-
- // Handle case where extension creates multiple instances.
- classIds.AddRange(GetCreateInstanceList(activation));
-
- return (cmdPalProvider, classIds);
- }
-
- private static async Task> GetInstalledAppExtensionsAsync() => await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync();
-
public async Task> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false)
{
await _getInstalledExtensionsLock.WaitAsync();
@@ -215,25 +308,22 @@ public async Task> RefreshInstalledExtensionsAsyn
}
}
- private static void UpdateExtensionsListsFromWrappers(List wrappers)
+ public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId)
{
- foreach (var extensionWrapper in wrappers)
- {
- // var localSettingsService = Application.Current.GetService();
- var extensionUniqueId = extensionWrapper.ExtensionUniqueId;
- var isExtensionDisabled = false; // await localSettingsService.ReadSettingAsync(extensionUniqueId + "-ExtensionDisabled");
+ var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
+ return extension.FirstOrDefault();
+ }
- _installedExtensions.Add(extensionWrapper);
- if (!isExtensionDisabled)
- {
- _enabledExtensions.Add(extensionWrapper);
- }
+ public void EnableExtension(string extensionUniqueId)
+ {
+ var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
+ _enabledExtensions.Add(extension.First());
+ }
- // TelemetryFactory.Get().Log(
- // "Extension_ReportInstalled",
- // LogLevel.Critical,
- // new ReportInstalledExtensionEvent(extensionUniqueId, isEnabled: !isExtensionDisabled));
- }
+ public void DisableExtension(string extensionUniqueId)
+ {
+ var extension = _enabledExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
+ _enabledExtensions.Remove(extension.First());
}
private static async Task> GetInstalledExtensionsAsyncUnderLock(bool includeDisabledExtensions, bool refresh)
@@ -295,6 +385,8 @@ private static async Task RebuildInstalledExtensionsCacheAsync()
}
}
+ private static async Task> GetInstalledAppExtensionsAsync() => await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync();
+
private static async Task> CreateWrappersForExtension(AppExtension extension)
{
var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension);
@@ -337,7 +429,6 @@ private static ExtensionWrapper CreateExtensionWrapper(AppExtension extension, I
}
else
{
- // log warning that extension declared unsupported extension interface
CommandPaletteHost.Instance.DebugLog($"Extension {extension.DisplayName} declared an unsupported interface: {supportedInterface.Key}");
}
}
@@ -346,77 +437,68 @@ private static ExtensionWrapper CreateExtensionWrapper(AppExtension extension, I
return extensionWrapper;
}
- public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId)
- {
- var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
- return extension.FirstOrDefault();
- }
-
- public async Task SignalStopExtensionsAsync()
+ private static void UpdateExtensionsListsFromWrappers(List wrappers)
{
- var installedExtensions = await GetInstalledExtensionsAsync();
- foreach (var installedExtension in installedExtensions)
+ foreach (var extensionWrapper in wrappers)
{
- Logger.LogDebug($"Signaling dispose to {installedExtension.ExtensionUniqueId}");
- try
- {
- if (installedExtension.IsRunning())
- {
- installedExtension.SignalDispose();
- }
- }
- catch (Exception ex)
+ var extensionUniqueId = extensionWrapper.ExtensionUniqueId;
+ var isExtensionDisabled = false;
+
+ _installedExtensions.Add(extensionWrapper);
+ if (!isExtensionDisabled)
{
- Logger.LogError($"Failed to send dispose signal to extension {installedExtension.ExtensionUniqueId}", ex);
+ _enabledExtensions.Add(extensionWrapper);
}
}
}
- public async Task> GetInstalledExtensionsAsync(ProviderType providerType, bool includeDisabledExtensions = false)
+ private static async Task IsValidCmdPalExtension(Package package)
{
- var installedExtensions = await GetInstalledExtensionsAsync(includeDisabledExtensions);
-
- List filteredExtensions = [];
- foreach (var installedExtension in installedExtensions)
+ var extensions = await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync();
+ foreach (var extension in extensions)
{
- if (installedExtension.HasProviderType(providerType))
+ if (package.Id?.FullName == extension.Package?.Id?.FullName)
{
- filteredExtensions.Add(installedExtension);
+ var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
+
+ return new(cmdPalProvider is not null && classId.Count != 0, extension);
}
}
- return filteredExtensions;
+ return new(false, null);
}
- public void Dispose()
+ private static async Task<(IPropertySet? CmdPalProvider, List ClassIds)> GetCmdPalExtensionPropertiesAsync(AppExtension extension)
{
- Dispose(disposing: true);
- GC.SuppressFinalize(this);
- }
+ var classIds = new List();
+ var properties = await extension.GetExtensionPropertiesAsync();
- protected virtual void Dispose(bool disposing)
- {
- if (!_disposedValue)
+ if (properties is null)
{
- if (disposing)
- {
- _getInstalledExtensionsLock.Dispose();
- _getInstalledWidgetsLock.Dispose();
- }
+ return (null, classIds);
+ }
- _disposedValue = true;
+ var cmdPalProvider = GetSubPropertySet(properties, "CmdPalProvider");
+ if (cmdPalProvider is null)
+ {
+ return (null, classIds);
}
+
+ var activation = GetSubPropertySet(cmdPalProvider, "Activation");
+ if (activation is null)
+ {
+ return (cmdPalProvider, classIds);
+ }
+
+ classIds.AddRange(GetCreateInstanceList(activation));
+
+ return (cmdPalProvider, classIds);
}
private static IPropertySet? GetSubPropertySet(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as IPropertySet : null;
private static object[]? GetSubPropertySetArray(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as object[] : null;
- ///
- /// There are cases where the extension creates multiple COM instances.
- ///
- /// Activation property set object
- /// List of ClassId strings associated with the activation property
private static List GetCreateInstanceList(IPropertySet activationPropSet)
{
var propSetList = new List();
@@ -424,8 +506,6 @@ private static List GetCreateInstanceList(IPropertySet activationPropSet
if (singlePropertySet is not null)
{
var classId = GetProperty(singlePropertySet, ClassIdProperty);
-
- // If the instance has a classId as a single string, then it's only supporting a single instance.
if (classId is not null)
{
propSetList.Add(classId);
@@ -457,35 +537,24 @@ private static List GetCreateInstanceList(IPropertySet activationPropSet
private static string? GetProperty(IPropertySet propSet, string name) => propSet[name] as string;
- public void EnableExtension(string extensionUniqueId)
+ public void Dispose()
{
- var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
- _enabledExtensions.Add(extension.First());
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
}
- public void DisableExtension(string extensionUniqueId)
+ private void Dispose(bool disposing)
{
- var extension = _enabledExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
- _enabledExtensions.Remove(extension.First());
- }
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ _getInstalledExtensionsLock.Dispose();
+ }
- /*
- /////
- //public async Task DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension)
- //{
- // // Only attempt to disable feature if its available.
- // if (IsWindowsOptionalFeatureAvailableForExtension(extension.ExtensionClassId))
- // {
- // return false;
- // }
- // _log.Warning($"Disabling extension: '{extension.ExtensionDisplayName}' because its feature is absent or unknown");
- // // Remove extension from list of enabled extensions to prevent Dev Home from re-querying for this extension
- // // for the rest of its process lifetime.
- // DisableExtension(extension.ExtensionUniqueId);
- // // Update the local settings so the next time the user launches Dev Home the extension will be disabled.
- // await _localSettingsService.SaveSettingAsync(extension.ExtensionUniqueId + "-ExtensionDisabled", true);
- // return true;
- //} */
+ _disposedValue = true;
+ }
+ }
}
internal record struct IsExtensionResult(bool IsExtension, AppExtension? Extension)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
index 3f4dbc6d328e..5fdc8c48f0a7 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
@@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -11,7 +10,6 @@
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Common.Helpers;
-using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
@@ -22,22 +20,20 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public sealed partial class TopLevelCommandManager : ObservableObject,
IRecipient,
+ IRecipient,
IRecipient,
IRecipient,
IRecipient,
IDisposable
{
- private static readonly TimeSpan ExtensionStartTimeout = TimeSpan.FromSeconds(10);
private static readonly TimeSpan CommandLoadTimeout = TimeSpan.FromSeconds(10);
- private static readonly TimeSpan BackgroundStartTimeout = TimeSpan.FromSeconds(60);
private static readonly TimeSpan BackgroundCommandLoadTimeout = TimeSpan.FromSeconds(60);
private readonly IServiceProvider _serviceProvider;
- private readonly ICommandProviderCache _commandProviderCache;
+ private readonly IEnumerable _extensionServices;
private readonly TaskScheduler _taskScheduler;
- private readonly List _builtInCommands = [];
- private readonly List _extensionCommandProviders = [];
+ private readonly List _commandProviders = [];
private readonly Lock _commandProvidersLock = new();
// watch out: if you add code that locks CommandProviders, be sure to always
@@ -50,18 +46,25 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
private HashSet<(string ProviderId, string CommandId)> _pinnedCommandSet = [];
- public TopLevelCommandManager(IServiceProvider serviceProvider, ICommandProviderCache commandProviderCache)
+ public TopLevelCommandManager(IServiceProvider serviceProvider, IEnumerable extensionServices)
{
_serviceProvider = serviceProvider;
- _commandProviderCache = commandProviderCache;
+ _extensionServices = extensionServices;
_currentExtensionLoadCancellationToken = _extensionLoadCts.Token;
_taskScheduler = _serviceProvider.GetService()!;
WeakReferenceMessenger.Default.Register(this);
+ WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
_reloadCommandsGate = new(ReloadAllCommandsAsyncCore);
RebuildPinnedCache();
+
+ foreach (var service in _extensionServices)
+ {
+ service.OnProviderAdded += ExtensionService_OnProviderAdded;
+ service.OnProviderRemoved += ExtensionService_OnProviderRemoved;
+ }
}
public ObservableCollection PinnedCommands { get; } = [];
@@ -84,7 +87,7 @@ public IEnumerable CommandProviders
{
lock (_commandProvidersLock)
{
- return _builtInCommands.Concat(_extensionCommandProviders).ToList();
+ return _commandProviders.ToList();
}
}
}
@@ -101,58 +104,6 @@ internal void RebuildPinnedCache()
ListHelpers.InPlaceUpdateList(PinnedCommands, settings.PinnedCommands);
}
- public async Task LoadBuiltinsAsync()
- {
- var s = new Stopwatch();
- s.Start();
-
- lock (_commandProvidersLock)
- {
- _builtInCommands.Clear();
- }
-
- // Load built-In commands first. These are all in-proc, and
- // owned by our ServiceProvider.
- var builtInCommands = _serviceProvider.GetServices();
- foreach (var provider in builtInCommands)
- {
- CommandProviderWrapper wrapper = new(provider, _taskScheduler);
- lock (_commandProvidersLock)
- {
- _builtInCommands.Add(wrapper);
- }
-
- var objects = await LoadTopLevelCommandsFromProvider(wrapper);
- lock (TopLevelCommands)
- {
- if (objects.Commands is IEnumerable commands)
- {
- foreach (var c in commands)
- {
- TopLevelCommands.Add(c);
- }
- }
- }
-
- lock (_dockBandsLock)
- {
- if (objects.DockBands is IEnumerable bands)
- {
- foreach (var c in bands)
- {
- DockBands.Add(c);
- }
- }
- }
- }
-
- s.Stop();
-
- Logger.LogDebug($"Loading built-ins took {s.ElapsedMilliseconds}ms");
-
- return true;
- }
-
// May be called from a background thread
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{
@@ -288,125 +239,197 @@ public async Task ReloadAllCommandsAsync()
{
// gate ensures that the reload is serialized and if multiple calls
// request a reload, only the first and the last one will be executed.
- // this should be superseded with a cancellable version.
await _reloadCommandsGate.ExecuteAsync(CancellationToken.None);
}
- private async Task ReloadAllCommandsAsyncCore(CancellationToken cancellationToken)
+ ///
+ /// Loads only built-in (in-process) command providers. This is fast and
+ /// suitable for the initial pre-load phase so the UI appears immediately.
+ ///
+ public async Task LoadBuiltInProvidersAsync()
{
- IsLoading = true;
-
- // Invalidate any background continuations from the previous load cycle
- await _extensionLoadCts.CancelAsync().ConfigureAwait(false);
- _extensionLoadCts.Dispose();
- _extensionLoadCts = new();
- _currentExtensionLoadCancellationToken = _extensionLoadCts.Token;
-
- var extensionService = _serviceProvider.GetService()!;
- await extensionService.SignalStopExtensionsAsync().ConfigureAwait(false);
-
- lock (TopLevelCommands)
+ var ct = _currentExtensionLoadCancellationToken;
+ foreach (var service in _extensionServices.OfType())
{
- TopLevelCommands.Clear();
+ var wrappers = await service.LoadProvidersAsync(ct).ConfigureAwait(false);
+ await RegisterAndLoadCommandsAsync(wrappers, ct).ConfigureAwait(false);
}
+ }
- lock (_dockBandsLock)
+ ///
+ /// Loads external (out-of-process WinRT) command providers. Call this after
+ /// the root page is displayed so the UI is not blocked by extension startup.
+ /// Commands appear progressively via .
+ ///
+ [RelayCommand]
+ public async Task LoadExternalProvidersAsync()
+ {
+ IsLoading = true;
+ try
{
- DockBands.Clear();
+ var ct = _currentExtensionLoadCancellationToken;
+ foreach (var service in _extensionServices.Where(s => s is not BuiltInExtensionService))
+ {
+ var wrappers = await service.LoadProvidersAsync(ct).ConfigureAwait(false);
+ await RegisterAndLoadCommandsAsync(wrappers, ct).ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ IsLoading = false;
}
-
- await LoadBuiltinsAsync().ConfigureAwait(false);
- _ = Task.Run(LoadExtensionsAsync, cancellationToken);
}
- // Load commands from our extensions. Called on a background thread.
- // Currently, this
- // * queries the package catalog,
- // * starts all the extensions,
- // * then fetches the top-level commands from them.
- // TODO In the future, we'll probably abstract some of this away, to have
- // separate extension tracking vs stub loading.
- [RelayCommand]
- public async Task LoadExtensionsAsync()
+ private async Task ReloadAllCommandsAsyncCore(CancellationToken cancellationToken)
{
- var extensionService = _serviceProvider.GetService()!;
-
- extensionService.OnExtensionAdded -= ExtensionService_OnExtensionAdded;
- extensionService.OnExtensionRemoved -= ExtensionService_OnExtensionRemoved;
-
- var ct = _currentExtensionLoadCancellationToken;
+ IsLoading = true;
- var extensions = (await extensionService.GetInstalledExtensionsAsync().ConfigureAwait(false)).ToImmutableList();
- lock (_commandProvidersLock)
+ try
{
- _extensionCommandProviders.Clear();
- }
+ // Invalidate any background continuations from the previous load cycle
+ await _extensionLoadCts.CancelAsync().ConfigureAwait(false);
+ _extensionLoadCts.Dispose();
+ _extensionLoadCts = new();
+ _currentExtensionLoadCancellationToken = _extensionLoadCts.Token;
- await StartExtensionsAndGetCommands(extensions, ct).ConfigureAwait(false);
-
- extensionService.OnExtensionAdded += ExtensionService_OnExtensionAdded;
- extensionService.OnExtensionRemoved += ExtensionService_OnExtensionRemoved;
+ // Signal all services to stop their running providers
+ foreach (var service in _extensionServices)
+ {
+ await service.SignalStopAsync().ConfigureAwait(false);
+ }
- IsLoading = false;
+ lock (TopLevelCommands)
+ {
+ TopLevelCommands.Clear();
+ }
- // Send on the current thread; receivers should marshal to UI if needed
- WeakReferenceMessenger.Default.Send();
+ lock (_dockBandsLock)
+ {
+ DockBands.Clear();
+ }
- return true;
- }
+ lock (_commandProvidersLock)
+ {
+ _commandProviders.Clear();
+ }
- private void ExtensionService_OnExtensionAdded(IExtensionService sender, IEnumerable extensions)
- {
- var ct = _currentExtensionLoadCancellationToken;
+ var ct = _currentExtensionLoadCancellationToken;
- // When we get an extension install event, hop off to a BG thread
- _ = Task.Run(
- async () =>
+ // Load providers from each service sequentially (order matters: built-ins first)
+ foreach (var service in _extensionServices)
{
- // for each newly installed extension, start it and get commands
- // from it. One single package might have more than one
- // IExtensionWrapper in it.
- await StartExtensionsAndGetCommands(extensions, ct).ConfigureAwait(false);
- },
- ct);
+ var wrappers = await service.LoadProvidersAsync(ct).ConfigureAwait(false);
+ await RegisterAndLoadCommandsAsync(wrappers, ct).ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ IsLoading = false;
+ WeakReferenceMessenger.Default.Send();
+ }
}
- private async Task StartExtensionsAndGetCommands(IEnumerable extensions, CancellationToken ct)
+ private async Task UpdateProviderEnabledStateAsyncCore(string providerId, bool isEnabled)
{
- var timer = Stopwatch.StartNew();
-
- // Start all extensions in parallel
- var startResults = await Task.WhenAll(extensions.Select(TryStartExtensionAsync)).ConfigureAwait(false);
+ IsLoading = true;
- var startedWrappers = new List();
- foreach (var r in startResults)
+ try
{
- if (r.IsStarted)
+ // If disabled, we'll remove that providers commands from top level commands, dock bands, and pinned commands.
+ if (!isEnabled)
{
- startedWrappers.Add(r.Wrapper);
+ lock (TopLevelCommands)
+ {
+ var commandsToRemove = TopLevelCommands.Where(c => c.CommandProviderId == providerId).ToList();
+ foreach (var command in commandsToRemove)
+ {
+ TopLevelCommands.Remove(command);
+ }
+ }
+
+ lock (_dockBandsLock)
+ {
+ var dockBandsToRemove = DockBands.Where(b => b.CommandProviderId == providerId).ToList();
+ foreach (var band in dockBandsToRemove)
+ {
+ DockBands.Remove(band);
+ }
+ }
+
+ lock (PinnedCommands)
+ {
+ var pinnedToRemove = PinnedCommands.Where(p => p.ProviderId == providerId).ToList();
+ foreach (var command in pinnedToRemove)
+ {
+ PinnedCommands.Remove(command);
+ }
+ }
}
- else if (r.IsTimedOut)
+ else
{
- _ = StartExtensionWhenReadyAsync(r.Extension, r.PendingStartTask, r.Stopwatch, ct);
- }
- }
+ CommandProviderWrapper? provider;
+ lock (_commandProvidersLock)
+ {
+ provider = _commandProviders.FirstOrDefault(p => p.ProviderId == providerId);
+ }
- // Register started extensions and load their commands
- var loadSummary = await RegisterAndLoadCommandsAsync(startedWrappers, ct).ConfigureAwait(false);
+ if (provider != null)
+ {
+ await provider.LoadTopLevelCommands(_serviceProvider);
- timer.Stop();
- Logger.LogInfo($"Loaded {loadSummary.CommandCount} command(s) and {loadSummary.DockBandCount} band(s) from {startedWrappers.Count} extension(s) in {timer.ElapsedMilliseconds} ms");
+ lock (TopLevelCommands)
+ {
+ foreach (var command in provider.TopLevelItems)
+ {
+ if (!TopLevelCommands.Any(a => a.Id == command.Id))
+ {
+ TopLevelCommands.Add(command);
+ }
+ }
+
+ foreach (var item in provider.FallbackItems)
+ {
+ if (!TopLevelCommands.Any(a => a.Id == item.Id) && item.IsEnabled)
+ {
+ TopLevelCommands.Add(item);
+ }
+ }
+ }
+
+ lock (_dockBandsLock)
+ {
+ foreach (var band in provider.DockBandItems)
+ {
+ if (!DockBands.Any(a => a.Id == band.Id))
+ {
+ DockBands.Add(band);
+ }
+ }
+ }
+ }
+ else
+ {
+ Logger.LogWarning($"Could not find provider with id '{providerId}' to update enabled state.");
+ return;
+ }
+ }
+ }
+ finally
+ {
+ IsLoading = false;
+ }
}
- private async Task RegisterAndLoadCommandsAsync(ICollection wrappers, CancellationToken ct)
+ private async Task RegisterAndLoadCommandsAsync(IEnumerable wrappers, CancellationToken ct)
{
+ var wrapperList = wrappers.ToList();
lock (_commandProvidersLock)
{
- _extensionCommandProviders.AddRange(wrappers);
+ _commandProviders.AddRange(wrapperList);
}
// Load the commands from the providers in parallel
- var loadResults = await Task.WhenAll(wrappers.Select(w => TryLoadCommandsAsync(w, ct))).ConfigureAwait(false);
+ var loadResults = await Task.WhenAll(wrapperList.Select(w => TryLoadCommandsAsync(w, ct))).ConfigureAwait(false);
var totalCommands = 0;
var totalDockBands = 0;
@@ -463,7 +486,6 @@ private async Task RegisterAndLoadCommandsAsync(ICollect
// Fire background continuations for timed-out loads outside the lock
foreach (var r in timedOut)
{
- // It's weird to repeat the condition here, but it allows the compiler to track nullability of other properties
if (r.IsTimedOut)
{
_ = AppendCommandsWhenReadyAsync(r.Wrapper, r.PendingLoadTask, r.Stopwatch, ct);
@@ -473,60 +495,6 @@ private async Task RegisterAndLoadCommandsAsync(ICollect
return new RegisterAndLoadSummary(totalCommands, totalDockBands);
}
- private async Task TryStartExtensionAsync(IExtensionWrapper extension)
- {
- Logger.LogDebug($"Starting {extension.PackageFullName}");
- var sw = Stopwatch.StartNew();
- var ct = _currentExtensionLoadCancellationToken;
- var startTask = extension.StartExtensionAsync();
- try
- {
- await startTask.WaitAsync(ExtensionStartTimeout, ct).ConfigureAwait(false);
- Logger.LogInfo($"Started extension {extension.PackageFullName} in {sw.ElapsedMilliseconds} ms");
- return ExtensionStartResult.Started(extension, new CommandProviderWrapper(extension, _taskScheduler, _commandProviderCache));
- }
- catch (TimeoutException)
- {
- Logger.LogWarning($"Starting extension {extension.PackageFullName} timed out after {sw.ElapsedMilliseconds} ms, continuing in background");
- return ExtensionStartResult.TimedOut(extension, startTask, sw);
- }
- catch (OperationCanceledException)
- {
- Logger.LogDebug($"Starting extension {extension.PackageFullName} was cancelled after {sw.ElapsedMilliseconds} ms");
- return ExtensionStartResult.Failed(extension);
- }
- catch (Exception ex)
- {
- Logger.LogError($"Failed to start extension {extension.PackageFullName} after {sw.ElapsedMilliseconds} ms: {ex}");
- return ExtensionStartResult.Failed(extension);
- }
- }
-
- private async Task StartExtensionWhenReadyAsync(
- IExtensionWrapper extension,
- Task startTask,
- Stopwatch sw,
- CancellationToken ct)
- {
- try
- {
- await startTask.WaitAsync(BackgroundStartTimeout, ct).ConfigureAwait(false);
-
- var wrapper = new CommandProviderWrapper(extension, _taskScheduler, _commandProviderCache);
- Logger.LogInfo($"Late-started extension {extension.PackageFullName} in {sw.ElapsedMilliseconds} ms, loading commands and bands");
-
- await RegisterAndLoadCommandsAsync([wrapper], ct).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Reload happened -- discard stale results
- }
- catch (Exception ex)
- {
- Logger.LogError($"Background start/load of extension {extension.PackageFullName} failed after {sw.ElapsedMilliseconds} ms: {ex}");
- }
- }
-
private async Task TryLoadCommandsAsync(CommandProviderWrapper wrapper, CancellationToken ct)
{
var sw = Stopwatch.StartNew();
@@ -536,22 +504,22 @@ private async Task TryLoadCommandsAsync(CommandProviderWrappe
var result = await loadTask.WaitAsync(CommandLoadTimeout, ct).ConfigureAwait(false);
var commandCount = result.Commands?.Count ?? 0;
var dockBandCount = result.DockBands?.Count ?? 0;
- Logger.LogInfo($"Loaded {commandCount} command(s) and {dockBandCount} band(s) from {wrapper.ExtensionHost?.Extension?.PackageFullName} in {sw.ElapsedMilliseconds} ms");
+ Logger.LogInfo($"Loaded {commandCount} command(s) and {dockBandCount} band(s) from {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} in {sw.ElapsedMilliseconds} ms");
return CommandLoadResult.Loaded(wrapper, result);
}
catch (TimeoutException)
{
- Logger.LogWarning($"Loading commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName} timed out after {sw.ElapsedMilliseconds} ms, continuing in background");
+ Logger.LogWarning($"Loading commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} timed out after {sw.ElapsedMilliseconds} ms, continuing in background");
return CommandLoadResult.TimedOut(wrapper, loadTask, sw);
}
catch (OperationCanceledException)
{
- Logger.LogDebug($"Loading commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName} was cancelled after {sw.ElapsedMilliseconds} ms");
+ Logger.LogDebug($"Loading commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} was cancelled after {sw.ElapsedMilliseconds} ms");
return CommandLoadResult.Failed(wrapper);
}
catch (Exception ex)
{
- Logger.LogError($"Failed to load commands and bands for extension {wrapper.ExtensionHost?.Extension?.PackageFullName} after {sw.ElapsedMilliseconds} ms: {ex}");
+ Logger.LogError($"Failed to load commands and bands for {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} after {sw.ElapsedMilliseconds} ms: {ex}");
return CommandLoadResult.Failed(wrapper);
}
}
@@ -590,7 +558,7 @@ private async Task AppendCommandsWhenReadyAsync(
}
}
- Logger.LogInfo($"Late-loaded {commands?.Count ?? 0} command(s) and {dockBands?.Count ?? 0} band(s) from {wrapper.ExtensionHost?.Extension?.PackageFullName} in {sw.ElapsedMilliseconds} ms");
+ Logger.LogInfo($"Late-loaded {commands?.Count ?? 0} command(s) and {dockBands?.Count ?? 0} band(s) from {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} in {sw.ElapsedMilliseconds} ms");
}
catch (OperationCanceledException)
{
@@ -598,52 +566,63 @@ private async Task AppendCommandsWhenReadyAsync(
}
catch (Exception ex)
{
- Logger.LogError($"Background loading of commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName} failed after {sw.ElapsedMilliseconds} ms: {ex}");
+ Logger.LogError($"Background loading of commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} failed after {sw.ElapsedMilliseconds} ms: {ex}");
}
}
- private void ExtensionService_OnExtensionRemoved(IExtensionService sender, IEnumerable extensions)
+ private void ExtensionService_OnProviderAdded(IExtensionService sender, IEnumerable wrappers)
{
- // When we get an extension uninstall event, hop off to a BG thread
+ var ct = _currentExtensionLoadCancellationToken;
+
_ = Task.Run(
async () =>
{
- // Then find all the top-level commands that belonged to that extension
+ await RegisterAndLoadCommandsAsync(wrappers, ct).ConfigureAwait(false);
+ },
+ ct);
+ }
+
+ private void ExtensionService_OnProviderRemoved(IExtensionService sender, IEnumerable removedWrappers)
+ {
+ // When we get a provider removal event, hop off to a BG thread
+ _ = Task.Run(
+ async () =>
+ {
+ var removedProviderIds = new HashSet(removedWrappers.Select(w => w.ProviderId));
+
List commandsToRemove = [];
List bandsToRemove = [];
- foreach (var extension in extensions)
+
+ lock (TopLevelCommands)
{
- lock (TopLevelCommands)
+ foreach (var command in TopLevelCommands)
{
- foreach (var command in TopLevelCommands)
+ if (removedProviderIds.Contains(command.CommandProviderId))
{
- var host = command.ExtensionHost;
- if (host?.Extension == extension)
- {
- commandsToRemove.Add(command);
- }
+ commandsToRemove.Add(command);
}
}
+ }
- lock (_dockBandsLock)
+ lock (_dockBandsLock)
+ {
+ foreach (var band in DockBands)
{
- foreach (var band in DockBands)
+ if (removedProviderIds.Contains(band.CommandProviderId))
{
- var host = band.ExtensionHost;
- if (host?.Extension == extension)
- {
- bandsToRemove.Add(band);
- }
+ bandsToRemove.Add(band);
}
}
}
- // Then back on the UI thread (remember, TopLevelCommands is
- // Observable, so you can't touch it on the BG thread)...
+ lock (_commandProvidersLock)
+ {
+ _commandProviders.RemoveAll(w => removedProviderIds.Contains(w.ProviderId));
+ }
+
await Task.Factory.StartNew(
() =>
{
- // ... remove all the deleted commands.
lock (TopLevelCommands)
{
if (commandsToRemove.Count != 0)
@@ -715,6 +694,9 @@ public List GetDockBandsSnapshot()
public void Receive(ReloadCommandsMessage message) =>
_ = ReloadAllCommandsAsync();
+ public void Receive(ProviderEnabledStateChangedMessage message) =>
+ _ = UpdateProviderEnabledStateAsyncCore(message.ProviderId, message.IsEnabled);
+
public void Receive(PinCommandItemMessage message)
{
var wrapper = LookupProvider(message.ProviderId);
@@ -752,8 +734,7 @@ public void Receive(PinToDockMessage message)
{
lock (_commandProvidersLock)
{
- return _builtInCommands.FirstOrDefault(w => w.ProviderId == providerId)
- ?? _extensionCommandProviders.FirstOrDefault(w => w.ProviderId == providerId);
+ return _commandProviders.FirstOrDefault(w => w.ProviderId == providerId);
}
}
@@ -761,8 +742,7 @@ internal bool IsProviderActive(string id)
{
lock (_commandProvidersLock)
{
- return _builtInCommands.Any(wrapper => wrapper.Id == id && wrapper.IsActive)
- || _extensionCommandProviders.Any(wrapper => wrapper.Id == id && wrapper.IsActive);
+ return _commandProviders.Any(wrapper => wrapper.Id == id && wrapper.IsActive);
}
}
@@ -815,49 +795,18 @@ internal void PinDockBand(TopLevelViewModel bandVm)
public void Dispose()
{
+ foreach (var service in _extensionServices)
+ {
+ service.OnProviderAdded -= ExtensionService_OnProviderAdded;
+ service.OnProviderRemoved -= ExtensionService_OnProviderRemoved;
+ }
+
_extensionLoadCts.Cancel();
_extensionLoadCts.Dispose();
_reloadCommandsGate.Dispose();
GC.SuppressFinalize(this);
}
- private sealed class ExtensionStartResult
- {
- public IExtensionWrapper Extension { get; }
-
- public CommandProviderWrapper? Wrapper { get; private init; }
-
- public Task? PendingStartTask { get; private init; }
-
- public Stopwatch? Stopwatch { get; private init; }
-
- [MemberNotNullWhen(true, nameof(Wrapper))]
- public bool IsStarted => Wrapper is not null;
-
- [MemberNotNullWhen(true, nameof(PendingStartTask), nameof(Stopwatch))]
- public bool IsTimedOut => PendingStartTask is not null;
-
- private ExtensionStartResult(IExtensionWrapper extension)
- {
- Extension = extension;
- }
-
- public static ExtensionStartResult Started(IExtensionWrapper extension, CommandProviderWrapper wrapper)
- {
- return new ExtensionStartResult(extension) { Wrapper = wrapper };
- }
-
- public static ExtensionStartResult TimedOut(IExtensionWrapper extension, Task pendingStartTask, Stopwatch sw)
- {
- return new ExtensionStartResult(extension) { PendingStartTask = pendingStartTask, Stopwatch = sw };
- }
-
- public static ExtensionStartResult Failed(IExtensionWrapper extension)
- {
- return new ExtensionStartResult(extension);
- }
- }
-
private sealed class CommandLoadResult
{
public TopLevelObjectSets? TopLevelObjectSets { get; private init; }
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
index 9808478e5f1c..6616de62b9ac 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
@@ -264,7 +264,10 @@ private static void AddCoreServices(
// Core services
services.AddSingleton(appInfoService);
- services.AddSingleton();
+ // Load IExtensionServices here
+ services.AddSingleton();
+ services.AddSingleton();
+
services.AddSingleton();
services.AddSingleton();
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
index d82983fad273..9d57b96eff27 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
@@ -922,8 +922,11 @@ internal void MainWindow_Closed(object sender, WindowEventArgs args)
}
}
- var extensionService = serviceProvider.GetService()!;
- extensionService.SignalStopExtensionsAsync();
+ var extensionServices = serviceProvider.GetServices();
+ foreach (var extensionService in extensionServices)
+ {
+ extensionService.SignalStopAsync();
+ }
App.Current.Services.GetService()!.Destroy();
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysRootPageService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysRootPageService.cs
index f24d9273f684..012437b6ad46 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysRootPageService.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysRootPageService.cs
@@ -36,7 +36,7 @@ public PowerToysRootPageService(TopLevelCommandManager topLevelCommandManager, A
public async Task PreLoadAsync()
{
- await _tlcManager.LoadBuiltinsAsync();
+ await _tlcManager.LoadBuiltInProvidersAsync();
}
public Microsoft.CommandPalette.Extensions.IPage GetRootPage()
@@ -46,11 +46,11 @@ public Microsoft.CommandPalette.Extensions.IPage GetRootPage()
public async Task PostLoadRootPageAsync()
{
- // After loading built-ins, and starting navigation, kick off a thread to load extensions.
- _tlcManager.LoadExtensionsCommand.Execute(null);
+ // After loading built-ins, and starting navigation, kick off a thread to load external extensions.
+ _tlcManager.LoadExternalProvidersCommand.Execute(null);
- await _tlcManager.LoadExtensionsCommand.ExecutionTask!;
- if (_tlcManager.LoadExtensionsCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
+ await _tlcManager.LoadExternalProvidersCommand.ExecutionTask!;
+ if (_tlcManager.LoadExternalProvidersCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
{
// TODO: Handle failure case
}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/ExtensionGalleryViewModelTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/ExtensionGalleryViewModelTests.cs
index 8a82ea040e1a..03829108429c 100644
--- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/ExtensionGalleryViewModelTests.cs
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/ExtensionGalleryViewModelTests.cs
@@ -13,6 +13,7 @@
using Microsoft.CmdPal.Common.WinGet.Models;
using Microsoft.CmdPal.Common.WinGet.Services;
using Microsoft.CmdPal.UI.ViewModels.Gallery;
+using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -61,7 +62,7 @@ public async Task LoadAsync_DoesNotBlockOnSlowSynchronousInstalledStatusKickoff(
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
- extensionService.Object,
+ new[] { extensionService.Object },
NullLogger.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -118,7 +119,7 @@ public async Task RefreshCommand_RefreshesInstalledAndWinGetStateInBackground()
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
- extensionService.Object,
+ new[] { extensionService.Object },
NullLogger.Instance,
CreateGalleryExtensionViewModelFactory(winGetService.Object, winGetStatusService.Object),
winGetService.Object,
@@ -202,7 +203,7 @@ public async Task RefreshCommand_DoesNotBlockOnSlowSynchronousStatusRefreshKicko
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
- extensionService.Object,
+ new[] { extensionService.Object },
NullLogger.Instance,
CreateGalleryExtensionViewModelFactory(winGetService.Object, winGetStatusService.Object),
winGetService.Object,
@@ -250,7 +251,7 @@ public async Task LoadAsync_DoesNotShowFallbackCacheWarning_ForNormalCacheHits()
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
- extensionService.Object,
+ new[] { extensionService.Object },
NullLogger.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -292,7 +293,7 @@ public async Task LoadAsync_ShowsFallbackCacheWarning_WhenServiceFallsBackToCach
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
- extensionService.Object,
+ new[] { extensionService.Object },
NullLogger.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -324,7 +325,7 @@ public async Task LoadAsync_ShowsErrorSurface_WhenGalleryIsRateLimited()
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
- extensionService.Object,
+ new[] { extensionService.Object },
NullLogger.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -359,7 +360,7 @@ public async Task LoadAsync_ShowsGenericErrorSurface_WhenGalleryLoadFailsWithout
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
- extensionService.Object,
+ new[] { extensionService.Object },
NullLogger.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -389,7 +390,7 @@ public async Task SortByNameCommand_SortsEntriesByTitle()
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
- extensionService.Object,
+ new[] { extensionService.Object },
NullLogger.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -421,7 +422,7 @@ public async Task SortByAuthorCommand_SortsEntriesByAuthor()
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
- extensionService.Object,
+ new[] { extensionService.Object },
NullLogger.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -485,7 +486,7 @@ public async Task SortByInstallationStatusCommand_SortsUpdatesBeforeInstalledBef
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
- extensionService.Object,
+ new[] { extensionService.Object },
NullLogger.Instance,
CreateGalleryExtensionViewModelFactory(winGetPackageStatusService: winGetStatusService.Object),
winGetPackageManagerService: null,