From 5f124cec556afa05c9f5c18dc728e3d2635820bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Thu, 29 Jan 2026 04:19:25 +0100 Subject: [PATCH] CmdPal: Cache and show information for disabled command providers (#44278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request This PR adds a cache of command provider information so we can show providers even when the command provider isn’t loaded. It also updates the description for disabled extensions on the Extensions page to always include the extension name. Finally, it adds a placeholder icon for cases where an extension icon isn’t loaded. Note that this doesn’t address fully transparent icons that some extensions may inherit from the default template. Before: image After: image ## PR Checklist - [ ] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .../CommandProviderWrapper.cs | 60 ++++++--- .../ProviderSettingsViewModel.cs | 9 +- .../Services/CommandProviderCacheContainer.cs | 10 ++ .../Services/CommandProviderCacheItem.cs | 7 + ...ommandProviderCacheSerializationContext.cs | 13 ++ .../Services/DefaultCommandProviderCache.cs | 127 ++++++++++++++++++ .../Services/ICommandProviderCache.cs | 12 ++ .../TopLevelCommandManager.cs | 7 +- .../cmdpal/Microsoft.CmdPal.UI/App.xaml.cs | 1 + .../Assets/Icons/ExtensionIconPlaceholder.png | Bin 0 -> 12406 bytes .../Settings/ExtensionsPage.xaml | 24 +++- 11 files changed, 243 insertions(+), 27 deletions(-) create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheContainer.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheItem.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheSerializationContext.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/DefaultCommandProviderCache.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ICommandProviderCache.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Icons/ExtensionIconPlaceholder.png diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs index 939b42de14..d0f73f4a12 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs @@ -6,6 +6,7 @@ using ManagedCommon; using Microsoft.CmdPal.Core.Common.Services; using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels.Models; +using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.CommandPalette.Extensions; using Microsoft.Extensions.DependencyInjection; @@ -23,6 +24,8 @@ public sealed class CommandProviderWrapper private readonly TaskScheduler _taskScheduler; + private readonly ICommandProviderCache? _commandProviderCache; + public TopLevelViewModel[] TopLevelItems { get; private set; } = []; public TopLevelViewModel[] FallbackItems { get; private set; } = []; @@ -43,13 +46,7 @@ public sealed class CommandProviderWrapper public bool IsActive { get; private set; } - public string ProviderId - { - get - { - return string.IsNullOrEmpty(Extension?.ExtensionUniqueId) ? Id : Extension.ExtensionUniqueId; - } - } + public string ProviderId => string.IsNullOrEmpty(Extension?.ExtensionUniqueId) ? Id : Extension.ExtensionUniqueId; public CommandProviderWrapper(ICommandProvider provider, TaskScheduler mainThread) { @@ -77,9 +74,11 @@ public sealed class CommandProviderWrapper Logger.LogDebug($"Initialized command provider {ProviderId}"); } - public CommandProviderWrapper(IExtensionWrapper extension, TaskScheduler mainThread) + public CommandProviderWrapper(IExtensionWrapper extension, TaskScheduler mainThread, ICommandProviderCache commandProviderCache) { _taskScheduler = mainThread; + _commandProviderCache = commandProviderCache; + Extension = extension; ExtensionHost = new CommandPaletteHost(extension); if (!Extension.IsRunning()) @@ -128,30 +127,31 @@ public sealed class CommandProviderWrapper if (!isValid) { IsActive = false; + RecallFromCache(); return; } var settings = serviceProvider.GetService()!; - IsActive = GetProviderSettings(settings).IsEnabled; + var providerSettings = GetProviderSettings(settings); + IsActive = providerSettings.IsEnabled; if (!IsActive) { + RecallFromCache(); return; } - ICommandItem[]? commands = null; - IFallbackCommandItem[]? fallbacks = null; - + var displayInfoInitialized = false; try { var model = _commandProvider.Unsafe!; - Task t = new(model.TopLevelCommands); - t.Start(); - commands = await t.ConfigureAwait(false); + Task loadTopLevelCommandsTask = new(model.TopLevelCommands); + loadTopLevelCommandsTask.Start(); + var commands = await loadTopLevelCommandsTask.ConfigureAwait(false); // On a BG thread here - fallbacks = model.FallbackCommands(); + var fallbacks = model.FallbackCommands(); if (model is ICommandProvider2 two) { @@ -162,6 +162,13 @@ public sealed class CommandProviderWrapper DisplayName = model.DisplayName; Icon = new(model.Icon); Icon.InitializeProperties(); + displayInfoInitialized = true; + + // Update cached display name + if (_commandProviderCache is not null && Extension?.ExtensionUniqueId is not null) + { + _commandProviderCache.Memorize(Extension.ExtensionUniqueId, new CommandProviderCacheItem(model.DisplayName)); + } // Note: explicitly not InitializeProperties()ing the settings here. If // we do that, then we'd regress GH #38321 @@ -177,6 +184,25 @@ public sealed class CommandProviderWrapper Logger.LogError("Failed to load commands from extension"); Logger.LogError($"Extension was {Extension!.PackageFamilyName}"); Logger.LogError(e.ToString()); + + if (!displayInfoInitialized) + { + RecallFromCache(); + } + } + } + + private void RecallFromCache() + { + var cached = _commandProviderCache?.Recall(ProviderId); + if (cached is not null) + { + DisplayName = cached.DisplayName; + } + + if (string.IsNullOrWhiteSpace(DisplayName)) + { + DisplayName = Extension?.PackageDisplayName ?? Extension?.PackageFamilyName ?? ProviderId; } } @@ -185,7 +211,7 @@ public sealed class CommandProviderWrapper var settings = serviceProvider.GetService()!; var providerSettings = GetProviderSettings(settings); - Func makeAndAdd = (ICommandItem? i, bool fallback) => + var makeAndAdd = (ICommandItem? i, bool fallback) => { CommandItemViewModel commandItemViewModel = new(new(i), pageContext); TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs index 68e554e463..d6208b712a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs @@ -14,11 +14,13 @@ namespace Microsoft.CmdPal.UI.ViewModels; public partial class ProviderSettingsViewModel : ObservableObject { + private static readonly IconInfoViewModel EmptyIcon = new(null); + private readonly CommandProviderWrapper _provider; private readonly ProviderSettings _providerSettings; private readonly SettingsModel _settings; - private readonly Lock _initializeSettingsLock = new(); + private Task? _initializeSettingsTask; public ProviderSettingsViewModel( @@ -43,7 +45,7 @@ public partial class ProviderSettingsViewModel : ObservableObject HasFallbackCommands ? $"{ExtensionName}, {TopLevelCommands.Count} commands, {_provider.FallbackItems?.Length} fallback commands" : $"{ExtensionName}, {TopLevelCommands.Count} commands" : - Resources.builtin_disabled_extension; + $"{ExtensionName}, {Resources.builtin_disabled_extension}"; [MemberNotNullWhen(true, nameof(Extension))] public bool IsFromExtension => _provider.Extension is not null; @@ -52,7 +54,7 @@ public partial class ProviderSettingsViewModel : ObservableObject public string ExtensionVersion => IsFromExtension ? $"{Extension.Version.Major}.{Extension.Version.Minor}.{Extension.Version.Build}.{Extension.Version.Revision}" : string.Empty; - public IconInfoViewModel Icon => _provider.Icon; + public IconInfoViewModel Icon => IsEnabled ? _provider.Icon : EmptyIcon; [ObservableProperty] public partial bool LoadingSettings { get; set; } @@ -69,6 +71,7 @@ public partial class ProviderSettingsViewModel : ObservableObject WeakReferenceMessenger.Default.Send(new()); OnPropertyChanged(nameof(IsEnabled)); OnPropertyChanged(nameof(ExtensionSubtext)); + OnPropertyChanged(nameof(Icon)); } if (value == true) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheContainer.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheContainer.cs new file mode 100644 index 0000000000..0ab835ebb9 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheContainer.cs @@ -0,0 +1,10 @@ +// 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.Services; + +internal sealed class CommandProviderCacheContainer +{ + public Dictionary Cache { get; init; } = new(StringComparer.Ordinal); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheItem.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheItem.cs new file mode 100644 index 0000000000..8c86c28178 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheItem.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.Services; + +public record CommandProviderCacheItem(string DisplayName); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheSerializationContext.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheSerializationContext.cs new file mode 100644 index 0000000000..9b73cdd19d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/CommandProviderCacheSerializationContext.cs @@ -0,0 +1,13 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.CmdPal.UI.ViewModels.Services; + +[JsonSerializable(typeof(CommandProviderCacheItem))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(CommandProviderCacheContainer))] +[JsonSourceGenerationOptions(WriteIndented = true, PropertyNameCaseInsensitive = false)] +internal sealed partial class CommandProviderCacheSerializationContext : JsonSerializerContext; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/DefaultCommandProviderCache.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/DefaultCommandProviderCache.cs new file mode 100644 index 0000000000..5e8f790b12 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/DefaultCommandProviderCache.cs @@ -0,0 +1,127 @@ +// 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.Text.Json; +using ManagedCommon; +using Microsoft.CmdPal.Core.Common.Helpers; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.Services; + +public sealed partial class DefaultCommandProviderCache : ICommandProviderCache, IDisposable +{ + private const string CacheFileName = "commandProviderCache.json"; + + private readonly Dictionary _cache = new(StringComparer.Ordinal); + + private readonly Lock _sync = new(); + + private readonly SupersedingAsyncGate _saveGate; + + public DefaultCommandProviderCache() + { + _saveGate = new SupersedingAsyncGate(async _ => await TrySaveAsync().ConfigureAwait(false)); + TryLoad(); + } + + public void Memorize(string providerId, CommandProviderCacheItem item) + { + ArgumentNullException.ThrowIfNull(providerId); + + lock (_sync) + { + _cache[providerId] = item; + } + + _ = _saveGate.ExecuteAsync(); + } + + public CommandProviderCacheItem? Recall(string providerId) + { + ArgumentNullException.ThrowIfNull(providerId); + + lock (_sync) + { + _cache.TryGetValue(providerId, out var item); + return item; + } + } + + private static string GetCacheFilePath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + return Path.Combine(directory, CacheFileName); + } + + private void TryLoad() + { + try + { + var path = GetCacheFilePath(); + if (!File.Exists(path)) + { + return; + } + + var json = File.ReadAllText(path); + if (string.IsNullOrWhiteSpace(json)) + { + return; + } + + var loaded = JsonSerializer.Deserialize( + json, + CommandProviderCacheSerializationContext.Default.CommandProviderCacheContainer!); + if (loaded?.Cache is null) + { + return; + } + + _cache.Clear(); + foreach (var kvp in loaded.Cache) + { + if (!string.IsNullOrEmpty(kvp.Key) && kvp.Value is not null) + { + _cache[kvp.Key] = kvp.Value; + } + } + } + catch (Exception ex) + { + Logger.LogError("Failed to load command provider cache: ", ex); + } + } + + private async Task TrySaveAsync() + { + try + { + Dictionary snapshot; + lock (_sync) + { + snapshot = new Dictionary(_cache, StringComparer.Ordinal); + } + + var container = new CommandProviderCacheContainer + { + Cache = snapshot, + }; + + var path = GetCacheFilePath(); + var json = JsonSerializer.Serialize(container, CommandProviderCacheSerializationContext.Default.CommandProviderCacheContainer!); + await File.WriteAllTextAsync(path, json).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogError("Failed to save command provider cache: ", ex); + } + } + + public void Dispose() + { + _saveGate.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ICommandProviderCache.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ICommandProviderCache.cs new file mode 100644 index 0000000000..201e6baa0e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ICommandProviderCache.cs @@ -0,0 +1,12 @@ +// 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.Services; + +public interface ICommandProviderCache +{ + void Memorize(string providerId, CommandProviderCacheItem item); + + CommandProviderCacheItem? Recall(string providerId); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs index c113b508d3..4473a1e144 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs @@ -13,6 +13,7 @@ using Microsoft.CmdPal.Core.Common.Helpers; using Microsoft.CmdPal.Core.Common.Services; using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.Extensions.DependencyInjection; @@ -25,6 +26,7 @@ public partial class TopLevelCommandManager : ObservableObject, IDisposable { private readonly IServiceProvider _serviceProvider; + private readonly ICommandProviderCache _commandProviderCache; private readonly TaskScheduler _taskScheduler; private readonly List _builtInCommands = []; @@ -34,9 +36,10 @@ public partial class TopLevelCommandManager : ObservableObject, TaskScheduler IPageContext.Scheduler => _taskScheduler; - public TopLevelCommandManager(IServiceProvider serviceProvider) + public TopLevelCommandManager(IServiceProvider serviceProvider, ICommandProviderCache commandProviderCache) { _serviceProvider = serviceProvider; + _commandProviderCache = commandProviderCache; _taskScheduler = _serviceProvider.GetService()!; WeakReferenceMessenger.Default.Register(this); _reloadCommandsGate = new(ReloadAllCommandsAsyncCore); @@ -319,7 +322,7 @@ public partial class TopLevelCommandManager : ObservableObject, try { await extension.StartExtensionAsync().WaitAsync(TimeSpan.FromSeconds(10)); - return new CommandProviderWrapper(extension, _taskScheduler); + return new CommandProviderWrapper(extension, _taskScheduler, _commandProviderCache); } catch (Exception ex) { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index 09af333bbe..3d6a7ef634 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -178,6 +178,7 @@ public partial class App : Application, IDisposable services.AddSingleton(state); // Services + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Icons/ExtensionIconPlaceholder.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Icons/ExtensionIconPlaceholder.png new file mode 100644 index 0000000000000000000000000000000000000000..cf1cd5c9b6940e92d8302c2435208c0d6e0630de GIT binary patch literal 12406 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 - + TargetType="x:Boolean" + Value="{x:Bind Icon.IsSet, FallbackValue=x:False, Mode=OneWay}"> + + + + + + +